commit 402d63bd9af87a225fe20258ff2ef55b55b4648c
Author: 2dust <31833384+2dust@users.noreply.github.com>
Date: Thu Jan 12 14:35:38 2023 +0800
.
diff --git a/.github/ISSUE_TEMPLATE/bug_cn.md b/.github/ISSUE_TEMPLATE/bug_cn.md
new file mode 100644
index 00000000..eaa1bc38
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_cn.md
@@ -0,0 +1,34 @@
+---
+name: v2rayNG程序问题
+about: 创建一个报告来帮助我们改进
+---
+
+在提出问题前请先自行排除服务器端问题,同时也请通过搜索确认是否有人提出过相同问题。
+
+
+### 预期行为
+描述你认为应该发生什么
+
+### 实际行为
+描述实际发生了什么
+
+### 复现方法
+1.
+2.
+3.
+
+### 日志信息
+
+
+通过`adb logcat -s com.v2ray.ang GoLog V2rayConfigUtilGoLog Main`获取日志。请自行删减日志中可能出现的敏感信息。
+
+如果问题可重现,建议先执行`adb logcat -c`清空系统日志再执行上述命令,再操作重现问题。
+```
+在这里粘贴日志
+```
+
+
+### 环境信息
+
+### 额外信息(可选)
+
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
new file mode 100644
index 00000000..3cd47d28
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -0,0 +1,5 @@
+blank_issues_enabled: false
+contact_links:
+ - name: V2Ray程序问题
+ url: https://github.com/v2fly/v2ray-core/
+ about: 如果您有V2Ray而非v2rayNG的问题,请至这个链接讨论。
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 00000000..0963c509
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,5 @@
+*.dat
+*.jks
+V2rayNG/app/release/output.json
+.idea/
+.gradle/
diff --git a/AndroidLibV2rayLite/README.md b/AndroidLibV2rayLite/README.md
new file mode 100644
index 00000000..118eefcf
--- /dev/null
+++ b/AndroidLibV2rayLite/README.md
@@ -0,0 +1,20 @@
+# AndroidLibV2rayLite
+
+### Preparation
+- latest Ubuntu environment
+- At lease 30G free space
+- Get Repo [AndroidLibV2rayLite](https://github.com/2dust/AndroidLibV2rayLite) or [AndroidLibXrayLite](https://github.com/2dust/AndroidLibXrayLite)
+### Prepare Go
+- Go to https://golang.org/doc/install and install latest go
+- Make sure `go version` works as expected
+### Prepare gomobile
+- Go to https://pkg.go.dev/golang.org/x/mobile/cmd/gomobile and install gomobile
+- export PATH=$PATH:~/go/bin
+- Make sure `gomobile init` works as expected
+### Prepare NDK
+- Go to https://developer.android.com/ndk/downloads and install latest NDK
+- export PATH=$PATH:
+- Make sure `ndk-build -v` works as expected
+### Make
+- sudo apt install make
+- Read and understand [build script](https://github.com/2dust/AndroidLibV2rayLite/blob/master/Makefile)
diff --git a/CR.md b/CR.md
new file mode 100644
index 00000000..0ec148b6
--- /dev/null
+++ b/CR.md
@@ -0,0 +1,29 @@
+v2rayNG 隐私条款
+
+最后更新 2017-11-22
+
+v2rayNG 尊重并保护所有用户的个人隐私权,为此我们向大众公开这份隐私条款。**您使用 v2rayNG 即代表您以阅读并同意了这份条款,如果您不同意这份条款请立即停止使用并卸载 v2rayNG。**
+
+1. 信息收集
+
+ v2rayNG 软件自身不会发送任何信息到开发者,但是您下载软件的应用市场(如 Google Play)可能会收集关于应用运行状态的相关信息并提供给 v2rayNG 开发者。有关这些信息,请阅读您使用的应用市场所提供的隐私条款。
+
+ v2rayNG 软件中可能包含需要通过 IAP 支付解锁的功能,您的支付信息将由相关的 IAP 渠道进行处理,而我们对支付信息没有访问权。
+
+ 当您向 v2rayNG 开发者反馈软件运行中的错误时,开发者可能会要求您提供软件以及系统的日志以帮助确认问题的原因。因日志中可能包括敏感信息,此类信息只能由您自己操作发送。**我们不对任何传输服务的安全性和隐私性做任何明示或暗示的担保,请您在传送相关信息时选择可以您自身可以接受的方式。**
+
+2. 信息共享
+
+ 我们不会向任何第三方出售收集到的用户数据。我们可能向外部开发者提供信息以协助软件的开发,但是在提供信息之前我们会传达相关保密义务并确定其可以遵守。
+
+3. 信息存留
+
+ 除非有相关法律规定,我们会在 30 天内清除不需要继续使用的用户数据,或将统计数据整合为无法识别单个用户的综合报告。
+
+4. 信息泄露
+
+ 我们会使用合理的技术和安全手段尽力保护用户的数据,但是无法保证数据的绝对安全。如果我们确认数据发生了泄露,我们会在 7 天内通过可用的渠道通知用户。**您同意不向我们追责任何因不可抗力而造成的损失。**
+
+5. 条款修改
+
+ 我们保留修改这份隐私条款的权利,但是会确保在更新条款前至少 30 天通过我们的可用渠道和应用内提示来通知用户。**在新条款生效后继续使用软件即表示您同意修改后的隐私条款。**
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 00000000..94a9ed02
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,674 @@
+ GNU GENERAL PUBLIC LICENSE
+ Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc.
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+ The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users. We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors. You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+ To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights. Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received. You must make sure that they, too, receive
+or can get the source code. And you must show them these terms so they
+know their rights.
+
+ Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+ For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software. For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+ Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so. This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software. The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable. Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products. If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+ Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary. To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ TERMS AND CONDITIONS
+
+ 0. Definitions.
+
+ "This License" refers to version 3 of the GNU General Public License.
+
+ "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+ "The Program" refers to any copyrightable work licensed under this
+License. Each licensee is addressed as "you". "Licensees" and
+"recipients" may be individuals or organizations.
+
+ To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy. The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+ A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+ To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+ To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies. Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+ An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License. If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+ 1. Source Code.
+
+ The "source code" for a work means the preferred form of the work
+for making modifications to it. "Object code" means any non-source
+form of a work.
+
+ A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+ The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form. A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+ The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities. However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work. For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+ The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+ The Corresponding Source for a work in source code form is that
+same work.
+
+ 2. Basic Permissions.
+
+ All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met. This License explicitly affirms your unlimited
+permission to run the unmodified Program. The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work. This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+ You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force. You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright. Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+ Conveying under any other circumstances is permitted solely under
+the conditions stated below. Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+ No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+ When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+ 4. Conveying Verbatim Copies.
+
+ You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+ You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+ 5. Conveying Modified Source Versions.
+
+ You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+ a) The work must carry prominent notices stating that you modified
+ it, and giving a relevant date.
+
+ b) The work must carry prominent notices stating that it is
+ released under this License and any conditions added under section
+ 7. This requirement modifies the requirement in section 4 to
+ "keep intact all notices".
+
+ c) You must license the entire work, as a whole, under this
+ License to anyone who comes into possession of a copy. This
+ License will therefore apply, along with any applicable section 7
+ additional terms, to the whole of the work, and all its parts,
+ regardless of how they are packaged. This License gives no
+ permission to license the work in any other way, but it does not
+ invalidate such permission if you have separately received it.
+
+ d) If the work has interactive user interfaces, each must display
+ Appropriate Legal Notices; however, if the Program has interactive
+ interfaces that do not display Appropriate Legal Notices, your
+ work need not make them do so.
+
+ A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit. Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+ 6. Conveying Non-Source Forms.
+
+ You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+ a) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by the
+ Corresponding Source fixed on a durable physical medium
+ customarily used for software interchange.
+
+ b) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by a
+ written offer, valid for at least three years and valid for as
+ long as you offer spare parts or customer support for that product
+ model, to give anyone who possesses the object code either (1) a
+ copy of the Corresponding Source for all the software in the
+ product that is covered by this License, on a durable physical
+ medium customarily used for software interchange, for a price no
+ more than your reasonable cost of physically performing this
+ conveying of source, or (2) access to copy the
+ Corresponding Source from a network server at no charge.
+
+ c) Convey individual copies of the object code with a copy of the
+ written offer to provide the Corresponding Source. This
+ alternative is allowed only occasionally and noncommercially, and
+ only if you received the object code with such an offer, in accord
+ with subsection 6b.
+
+ d) Convey the object code by offering access from a designated
+ place (gratis or for a charge), and offer equivalent access to the
+ Corresponding Source in the same way through the same place at no
+ further charge. You need not require recipients to copy the
+ Corresponding Source along with the object code. If the place to
+ copy the object code is a network server, the Corresponding Source
+ may be on a different server (operated by you or a third party)
+ that supports equivalent copying facilities, provided you maintain
+ clear directions next to the object code saying where to find the
+ Corresponding Source. Regardless of what server hosts the
+ Corresponding Source, you remain obligated to ensure that it is
+ available for as long as needed to satisfy these requirements.
+
+ e) Convey the object code using peer-to-peer transmission, provided
+ you inform other peers where the object code and Corresponding
+ Source of the work are being offered to the general public at no
+ charge under subsection 6d.
+
+ A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+ A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling. In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage. For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product. A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+ "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source. The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+ If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information. But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+ The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed. Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+ Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+ 7. Additional Terms.
+
+ "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law. If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+ When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it. (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.) You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+ Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+ a) Disclaiming warranty or limiting liability differently from the
+ terms of sections 15 and 16 of this License; or
+
+ b) Requiring preservation of specified reasonable legal notices or
+ author attributions in that material or in the Appropriate Legal
+ Notices displayed by works containing it; or
+
+ c) Prohibiting misrepresentation of the origin of that material, or
+ requiring that modified versions of such material be marked in
+ reasonable ways as different from the original version; or
+
+ d) Limiting the use for publicity purposes of names of licensors or
+ authors of the material; or
+
+ e) Declining to grant rights under trademark law for use of some
+ trade names, trademarks, or service marks; or
+
+ f) Requiring indemnification of licensors and authors of that
+ material by anyone who conveys the material (or modified versions of
+ it) with contractual assumptions of liability to the recipient, for
+ any liability that these contractual assumptions directly impose on
+ those licensors and authors.
+
+ All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10. If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term. If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+ If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+ Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+ 8. Termination.
+
+ You may not propagate or modify a covered work except as expressly
+provided under this License. Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+ However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+ Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+ Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License. If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+ 9. Acceptance Not Required for Having Copies.
+
+ You are not required to accept this License in order to receive or
+run a copy of the Program. Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance. However,
+nothing other than this License grants you permission to propagate or
+modify any covered work. These actions infringe copyright if you do
+not accept this License. Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+ 10. Automatic Licensing of Downstream Recipients.
+
+ Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License. You are not responsible
+for enforcing compliance by third parties with this License.
+
+ An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations. If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+ You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License. For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+ 11. Patents.
+
+ A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based. The
+work thus licensed is called the contributor's "contributor version".
+
+ A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version. For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+ Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+ In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement). To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+ If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+ If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+ A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License. You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+ Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+ 12. No Surrender of Others' Freedom.
+
+ If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all. For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+ 13. Use with the GNU Affero General Public License.
+
+ Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work. The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+ 14. Revised Versions of this License.
+
+ The Free Software Foundation may publish revised and/or new versions of
+the GNU General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the
+Program specifies that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation. If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+ If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+ Later license versions may give you additional or different
+permissions. However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+ 15. Disclaimer of Warranty.
+
+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. Limitation of Liability.
+
+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+ 17. Interpretation of Sections 15 and 16.
+
+ If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+
+ Copyright (C)
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see .
+
+Also add information on how to contact you by electronic and paper mail.
+
+ If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+ Copyright (C)
+ This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, your program's commands
+might be different; for a GUI interface, you would use an "about box".
+
+ You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU GPL, see
+.
+
+ The GNU General Public License does not permit incorporating your program
+into proprietary programs. If your program is a subroutine library, you
+may consider it more useful to permit linking proprietary applications with
+the library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License. But first, please read
+.
diff --git a/README.md b/README.md
new file mode 100644
index 00000000..34c837dc
--- /dev/null
+++ b/README.md
@@ -0,0 +1,36 @@
+# v2rayNG
+
+A V2Ray client for Android, support [Xray core](https://github.com/XTLS/Xray-core) and [v2fly core](https://github.com/v2fly/v2ray-core)
+
+[](https://developer.android.com/about/versions/lollipop)
+[](https://kotlinlang.org)
+[](https://github.com/2dust/v2rayNG/commits/master)
+[](https://www.codefactor.io/repository/github/2dust/v2rayng)
+[](https://github.com/2dust/v2rayNG/releases)
+[](https://t.me/v2rayn)
+
+
+
+
+
+### Telegram Channel
+[github_2dust](https://t.me/github_2dust)
+
+### Usage
+
+#### Geoip and Geosite
+- geoip.dat and geosite.dat files are in `Android/data/com.v2ray.ang/files/assets` (path may differ on some Android device)
+- download feature will get enhanced version in this [repo](https://github.com/Loyalsoldier/v2ray-rules-dat) (Note it need a working proxy)
+- latest official [domain list](https://github.com/v2fly/domain-list-community) and [ip list](https://github.com/v2fly/geoip) can be imported manually
+- possible to use third party dat file in the same folder, like [h2y](https://guide.v2fly.org/routing/sitedata.html#%E5%A4%96%E7%BD%AE%E7%9A%84%E5%9F%9F%E5%90%8D%E6%96%87%E4%BB%B6)
+
+### More in our [wiki](https://github.com/2dust/v2rayNG/wiki)
+
+### Development guide
+
+Android project under V2rayNG folder can be compiled directly in Android Studio, or using Gradle wrapper. But the v2ray core inside the aar is (probably) outdated.
+The aar can be compiled from the Golang project [AndroidLibV2rayLite](https://github.com/2dust/AndroidLibV2rayLite) or [AndroidLibXrayLite](https://github.com/2dust/AndroidLibXrayLite).
+For a quick start, read guide for [Go Mobile](https://github.com/golang/go/wiki/Mobile) and [Makefiles for Go Developers](https://tutorialedge.net/golang/makefiles-for-go-developers/)
+
+v2rayNG can run on Android Emulators. For WSA, VPN permission need to be granted via
+`appops set [package name] ACTIVATE_VPN allow`
diff --git a/V2rayNG/.gitignore b/V2rayNG/.gitignore
new file mode 100644
index 00000000..16eb6ec8
--- /dev/null
+++ b/V2rayNG/.gitignore
@@ -0,0 +1,10 @@
+*.iml
+.gradle
+/local.properties
+/.idea
+.DS_Store
+/build
+/captures
+*.apk
+signing.properties
+*.aar
diff --git a/V2rayNG/app/.gitignore b/V2rayNG/app/.gitignore
new file mode 100644
index 00000000..2abde4aa
--- /dev/null
+++ b/V2rayNG/app/.gitignore
@@ -0,0 +1,2 @@
+/build
+/google-services.json
diff --git a/V2rayNG/app/build.gradle b/V2rayNG/app/build.gradle
new file mode 100644
index 00000000..3600fe3b
--- /dev/null
+++ b/V2rayNG/app/build.gradle
@@ -0,0 +1,151 @@
+apply plugin: 'com.android.application'
+apply plugin: 'kotlin-android'
+
+Properties props = new Properties()
+props.load(new FileInputStream(new File('local.properties')))
+
+android {
+ compileSdkVersion Integer.parseInt("$compileSdkVer")
+ buildToolsVersion "$buildToolsVer"
+
+ compileOptions {
+ targetCompatibility = "8"
+ sourceCompatibility = "8"
+ }
+
+ defaultConfig {
+ applicationId "com.v2ray.ang"
+ minSdkVersion 21
+ targetSdkVersion Integer.parseInt("$targetSdkVer")
+ multiDexEnabled true
+ versionCode 495
+ versionName "1.7.33"
+ }
+
+ if (props["sign"]) {
+ signingConfigs {
+ release {
+ storeFile file("../key.jks")
+ keyAlias 'ang'
+ keyPassword '123456'
+ storePassword '123456'
+ }
+ debug {
+ storeFile file("../key.jks")
+ keyAlias 'ang'
+ keyPassword '123456'
+ storePassword '123456'
+ }
+ }
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ zipAlignEnabled false
+ shrinkResources false
+ if (props["sign"]) {
+ signingConfig signingConfigs.release
+ }
+ ndk.abiFilters 'x86', 'x86_64', 'armeabi-v7a', 'arm64-v8a'
+// proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+ }
+ debug {
+ minifyEnabled false
+ zipAlignEnabled false
+ shrinkResources false
+ if (props["sign"]) {
+ signingConfig signingConfigs.release
+ }
+ ndk.abiFilters 'x86', 'x86_64', 'armeabi-v7a', 'arm64-v8a'
+ }
+ }
+
+ sourceSets {
+ main {
+ jniLibs.srcDirs = ['libs']
+ java.srcDirs += 'src/main/kotlin'
+ }
+ }
+
+ kotlinOptions {
+ jvmTarget = JavaVersion.VERSION_1_8
+ }
+
+ splits {
+ abi {
+ enable true
+ reset()
+ include 'x86', 'x86_64', 'armeabi-v7a', 'arm64-v8a' //select ABIs to build APKs for
+ universalApk true //generate an additional APK that contains all the ABIs
+ }
+ }
+
+ // map for the version code
+ project.ext.versionCodes = ['armeabi-v7a': 1, 'arm64-v8a': 2, 'x86': 3, 'x86_64': 4]
+
+ android.applicationVariants.all { variant ->
+ // assign different version code for each output
+ variant.outputs.each { output ->
+ output.outputFileName = "v2rayNG_" + variant.versionName + "_" + output.getFilter(com.android.build.OutputFile.ABI) + ".apk"
+
+ output.versionCodeOverride =
+ project.ext.versionCodes.get(output.getFilter(com.android.build.OutputFile.ABI), 0) *
+ 1000000 + android.defaultConfig.versionCode
+ }
+ }
+
+ buildFeatures {
+ viewBinding true
+ }
+}
+
+dependencies {
+ implementation fileTree(dir: 'libs', include: ['*.aar', '*.jar'], exclude: [])
+ testImplementation 'junit:junit:4.13.2'
+
+ // Androidx
+ implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
+ implementation 'androidx.legacy:legacy-support-v4:1.0.0'
+ implementation 'androidx.appcompat:appcompat:1.4.2'
+ implementation 'com.google.android.material:material:1.6.1'
+ implementation 'androidx.cardview:cardview:1.0.0'
+ implementation 'androidx.preference:preference-ktx:1.2.0'
+ implementation 'androidx.recyclerview:recyclerview:1.2.1'
+ implementation 'androidx.fragment:fragment-ktx:1.5.2'
+ implementation 'androidx.multidex:multidex:2.0.1'
+ implementation 'androidx.viewpager2:viewpager2:1.1.0-beta01'
+
+ // Androidx ktx
+ implementation 'androidx.activity:activity-ktx:1.5.1'
+ implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1'
+ implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.5.1'
+ implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.5.1'
+
+ //kotlin
+ implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlinVersion"
+ implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2"
+ implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2"
+
+ implementation 'com.tencent:mmkv-static:1.2.12'
+ implementation 'com.google.code.gson:gson:2.8.9'
+ implementation 'io.reactivex:rxjava:1.3.4'
+ implementation 'io.reactivex:rxandroid:1.2.1'
+ implementation 'com.tbruyelle.rxpermissions:rxpermissions:0.9.4@aar'
+ implementation 'me.dm7.barcodescanner:core:1.9.8'
+ implementation 'me.dm7.barcodescanner:zxing:1.9.8'
+ implementation 'com.github.jorgecastilloprz:fabprogresscircle:1.01@aar'
+ implementation 'me.drakeet.support:toastcompat:1.1.0'
+ implementation 'com.blacksquircle.ui:editorkit:2.1.1'
+ implementation 'com.blacksquircle.ui:language-base:2.1.1'
+ implementation 'com.blacksquircle.ui:language-json:2.1.1'
+}
+
+//buildscript {
+// repositories {
+// google()
+// mavenCentral()
+// maven { url 'https://maven.google.com' }
+// maven { url 'https://jitpack.io' }
+// }
+//}
diff --git a/V2rayNG/app/libs/arm64-v8a/libtun2socks.so b/V2rayNG/app/libs/arm64-v8a/libtun2socks.so
new file mode 100644
index 00000000..3dbdde7a
Binary files /dev/null and b/V2rayNG/app/libs/arm64-v8a/libtun2socks.so differ
diff --git a/V2rayNG/app/libs/armeabi-v7a/libtun2socks.so b/V2rayNG/app/libs/armeabi-v7a/libtun2socks.so
new file mode 100644
index 00000000..79e3a96a
Binary files /dev/null and b/V2rayNG/app/libs/armeabi-v7a/libtun2socks.so differ
diff --git a/V2rayNG/app/libs/x86/libtun2socks.so b/V2rayNG/app/libs/x86/libtun2socks.so
new file mode 100644
index 00000000..30a48fc2
Binary files /dev/null and b/V2rayNG/app/libs/x86/libtun2socks.so differ
diff --git a/V2rayNG/app/libs/x86_64/libtun2socks.so b/V2rayNG/app/libs/x86_64/libtun2socks.so
new file mode 100644
index 00000000..ce965f06
Binary files /dev/null and b/V2rayNG/app/libs/x86_64/libtun2socks.so differ
diff --git a/V2rayNG/app/proguard-rules.pro b/V2rayNG/app/proguard-rules.pro
new file mode 100644
index 00000000..e69de29b
diff --git a/V2rayNG/app/src/androidTest/java/com/v2ray/ang/ApplicationTest.java b/V2rayNG/app/src/androidTest/java/com/v2ray/ang/ApplicationTest.java
new file mode 100644
index 00000000..e221e714
--- /dev/null
+++ b/V2rayNG/app/src/androidTest/java/com/v2ray/ang/ApplicationTest.java
@@ -0,0 +1,13 @@
+package com.v2ray.ang;
+
+import android.app.Application;
+import android.test.ApplicationTestCase;
+
+/**
+ * Testing Fundamentals
+ */
+public class ApplicationTest extends ApplicationTestCase {
+ public ApplicationTest() {
+ super(Application.class);
+ }
+}
\ No newline at end of file
diff --git a/V2rayNG/app/src/main/AndroidManifest.xml b/V2rayNG/app/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..f90f1274
--- /dev/null
+++ b/V2rayNG/app/src/main/AndroidManifest.xml
@@ -0,0 +1,187 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/V2rayNG/app/src/main/assets/custom_routing_block b/V2rayNG/app/src/main/assets/custom_routing_block
new file mode 100644
index 00000000..a6ebf394
--- /dev/null
+++ b/V2rayNG/app/src/main/assets/custom_routing_block
@@ -0,0 +1 @@
+geosite:category-ads-all,
\ No newline at end of file
diff --git a/V2rayNG/app/src/main/assets/custom_routing_direct b/V2rayNG/app/src/main/assets/custom_routing_direct
new file mode 100644
index 00000000..5408992c
--- /dev/null
+++ b/V2rayNG/app/src/main/assets/custom_routing_direct
@@ -0,0 +1,132 @@
+domain:12306.com,
+domain:51ym.me,
+domain:52pojie.cn,
+domain:8686c.com,
+domain:abercrombie.com,
+domain:adobesc.com,
+domain:air-matters.com,
+domain:air-matters.io,
+domain:airtable.com,
+domain:akadns.net,
+domain:apache.org,
+domain:api.crisp.chat,
+domain:api.termius.com,
+domain:appshike.com,
+domain:appstore.com,
+domain:aweme.snssdk.com,
+domain:bababian.com,
+domain:battle.net,
+domain:beatsbydre.com,
+domain:bet365.com,
+domain:bilibili.cn,
+domain:ccgslb.com,
+domain:ccgslb.net,
+domain:chunbo.com,
+domain:chunboimg.com,
+domain:clashroyaleapp.com,
+domain:cloudsigma.com,
+domain:cloudxns.net,
+domain:cmfu.com,
+domain:culturedcode.com,
+domain:dct-cloud.com,
+domain:didialift.com,
+domain:douyutv.com,
+domain:duokan.com,
+domain:dytt8.net,
+domain:easou.com,
+domain:ecitic.net,
+domain:eclipse.org,
+domain:eudic.net,
+domain:ewqcxz.com,
+domain:fir.im,
+domain:frdic.com,
+domain:fresh-ideas.cc,
+domain:godic.net,
+domain:goodread.com,
+domain:haibian.com,
+domain:hdslb.net,
+domain:hollisterco.com,
+domain:hongxiu.com,
+domain:hxcdn.net,
+domain:images.unsplash.com,
+domain:img4me.com,
+domain:ipify.org,
+domain:ixdzs.com,
+domain:jd.hk,
+domain:jianshuapi.com,
+domain:jomodns.com,
+domain:jsboxbbs.com,
+domain:knewone.com,
+domain:kuaidi100.com,
+domain:lemicp.com,
+domain:letvcloud.com,
+domain:lizhi.io,
+domain:localizecdn.com,
+domain:lucifr.com,
+domain:luoo.net,
+domain:mai.tn,
+domain:maven.org,
+domain:miwifi.com,
+domain:moji.com,
+domain:moke.com,
+domain:mtalk.google.com,
+domain:mxhichina.com,
+domain:myqcloud.com,
+domain:myunlu.com,
+domain:netease.com,
+domain:nfoservers.com,
+domain:nssurge.com,
+domain:nuomi.com,
+domain:ourdvs.com,
+domain:overcast.fm,
+domain:paypal.com,
+domain:paypalobjects.com,
+domain:pgyer.com,
+domain:qdaily.com,
+domain:qdmm.com,
+domain:qin.io,
+domain:qingmang.me,
+domain:qingmang.mobi,
+domain:qqurl.com,
+domain:rarbg.to,
+domain:rrmj.tv,
+domain:ruguoapp.com,
+domain:sm.ms,
+domain:snwx.com,
+domain:soku.com,
+domain:startssl.com,
+domain:store.steampowered.com,
+domain:symcd.com,
+domain:teamviewer.com,
+domain:tmzvps.com,
+domain:trello.com,
+domain:trellocdn.com,
+domain:ttmeiju.com,
+domain:udache.com,
+domain:uxengine.net,
+domain:weather.bjango.com,
+domain:weather.com,
+domain:webqxs.com,
+domain:weico.cc,
+domain:wenku8.net,
+domain:werewolf.53site.com,
+domain:windowsupdate.com,
+domain:wkcdn.com,
+domain:workflowy.com,
+domain:xdrig.com,
+domain:xiaojukeji.com,
+domain:xiaomi.net,
+domain:xiaomicp.com,
+domain:ximalaya.com,
+domain:xitek.com,
+domain:xmcdn.com,
+domain:xslb.net,
+domain:xteko.com,
+domain:yach.me,
+domain:yixia.com,
+domain:yunjiasu-cdn.net,
+domain:zealer.com,
+domain:zgslb.net,
+domain:zimuzu.tv,
+domain:zmz002.com,
+domain:samsungdm.com,
\ No newline at end of file
diff --git a/V2rayNG/app/src/main/assets/custom_routing_proxy b/V2rayNG/app/src/main/assets/custom_routing_proxy
new file mode 100644
index 00000000..d67a6237
--- /dev/null
+++ b/V2rayNG/app/src/main/assets/custom_routing_proxy
@@ -0,0 +1,33 @@
+geosite:google,
+geosite:github,
+geosite:netflix,
+geosite:steam,
+geosite:telegram,
+geosite:tumblr,
+geosite:speedtest,
+geosite:bbc,
+domain:gvt1.com,
+domain:textnow.com,
+domain:twitch.tv,
+domain:wikileaks.org,
+domain:naver.com,
+91.108.4.0/22,
+91.108.8.0/22,
+91.108.12.0/22,
+91.108.20.0/22,
+91.108.36.0/23,
+91.108.38.0/23,
+91.108.56.0/22,
+149.154.160.0/20,
+149.154.164.0/22,
+149.154.172.0/22,
+74.125.0.0/16,
+173.194.0.0/16,
+172.217.0.0/16,
+216.58.200.0/24,
+216.58.220.0/24,
+91.108.56.116,
+91.108.56.0/24,
+109.239.140.0/24,
+149.154.167.0/24,
+149.154.175.0/24,
\ No newline at end of file
diff --git a/V2rayNG/app/src/main/assets/proxy_packagename.txt b/V2rayNG/app/src/main/assets/proxy_packagename.txt
new file mode 100644
index 00000000..ecac8251
--- /dev/null
+++ b/V2rayNG/app/src/main/assets/proxy_packagename.txt
@@ -0,0 +1,241 @@
+amanita_design.samorost3.gp
+android
+au.com.shiftyjelly.pocketcasts
+bbc.mobile.news.ww
+be.mygod.vpnhotspot
+ch.protonmail.android
+co.wanqu.android
+com.alphainventor.filemanager
+com.amazon.kindle
+com.amazon.mshop.android.shopping
+com.android.chrome
+com.android.providers.downloads
+com.android.providers.downloads.ui
+com.android.providers.telephony
+com.android.settings
+com.android.vending
+com.android6park.m6park
+com.apkpure.aegon
+com.apkupdater
+com.app.pornhub
+com.arthurivanets.owly
+com.asahi.tida.tablet
+com.authy.authy
+com.avmovie
+com.ballistiq.artstation
+com.binance.dev
+com.bitly.app
+com.brave.browser
+com.brave.browser_beta
+com.breel.wallpapers18
+com.bvanced.android.youtube
+com.chrome.beta
+com.chrome.canary
+com.chrome.dev
+com.cl.newt66y
+com.cradle.iitc_mobile
+com.cygames.shadowverse
+com.devhd.feedly
+com.devolver.reigns2
+com.discord
+com.downloader.video.tumblr
+com.driverbrowser
+com.dropbox.android
+com.duolingo
+com.duckduckgo.mobile.android
+com.dv.adm
+com.estrongs.android.pop
+com.estrongs.android.pop.pro
+com.evernote
+com.facebook.katana
+com.facebook.lite
+com.facebook.mlite
+com.facebook.orca
+com.facebook.services
+com.facebook.system
+com.fastaccess.github
+com.felixfilip.scpae
+com.fireproofstudios.theroom4
+com.firstrowria.pushnotificationtester
+com.flyersoft.moonreaderp
+com.fooview.android.fooview
+com.fvd.eversync
+com.gameloft.android.anmp.glofta8hm
+com.gameloft.android.anmp.glofta9hm
+com.gianlu.aria2app
+com.github.yeriomin.yalpstore
+com.google.android.apps.adm
+com.google.android.apps.books
+com.google.android.apps.docs
+com.google.android.apps.docs.editors.sheets
+com.google.android.apps.fitness
+com.google.android.apps.googleassistant
+com.google.android.apps.googlevoice
+com.google.android.apps.hangoutsdialer
+com.google.android.apps.inbox
+com.google.android.apps.magazines
+com.google.android.apps.maps
+com.google.android.apps.nbu.files
+com.google.android.apps.paidtasks
+com.google.android.apps.pdfviewer
+com.google.android.apps.photos
+com.google.android.apps.plus
+com.google.android.apps.translate
+com.google.android.gm
+com.google.android.gms
+com.google.android.gms.setup
+com.google.android.googlequicksearchbox
+com.google.android.gsf
+com.google.android.gsf.login
+com.google.android.ims
+com.google.android.inputmethod.latin
+com.google.android.instantapps.supervisor
+com.google.android.keep
+com.google.android.music
+com.google.android.ogyoutube
+com.google.android.partnersetup
+com.google.android.play.games
+com.google.android.street
+com.google.android.syncadapters.calendar
+com.google.android.syncadapters.contacts
+com.google.android.talk
+com.google.android.tts
+com.google.android.videos
+com.google.android.youtube
+com.google.ar.lens
+com.hochan.coldsoup
+com.ifttt.ifttt
+com.imgur.mobile
+com.innologica.inoreader
+com.instagram.android
+com.instapaper.android
+com.jarvanh.vpntether
+com.kapp.youtube.final
+com.klinker.android.twitter_l
+com.lastpass.lpandroid
+com.linecorp.linelite
+com.lingodeer
+com.mediapods.tumbpods
+com.mgoogle.android.gms
+com.microsoft.emmx
+com.microsoft.office.powerpoint
+com.microsoft.skydrive
+com.mixplorer
+com.msd.consumerchinese
+com.msd.professionalchinese
+com.mss2011c.sharehelper
+com.netflix.mediaclient
+com.newin.nplayer.pro
+com.nianticlabs.ingress.prime.qa
+com.nianticproject.ingress
+com.ninefolders.hd3
+com.ninegag.android.app
+com.nintendo.zara
+com.nytimes.cn
+com.oasisfeng.island
+com.ocnt.liveapp.hw
+com.orekie.search
+com.patreon.android
+com.paypal.android.p2pmobile
+com.perol.asdpl.pixivez
+com.pinterest
+com.popularapp.periodcalendar
+com.popularapp.videodownloaderforinstagram
+com.pushbullet.android
+com.quoord.tapatalkpro.activity
+com.quora.android
+com.rayark.cytus2
+com.rayark.implosion
+com.rayark.pluto
+com.reddit.frontpage
+com.resilio.sync
+com.rhmsoft.edit
+com.rubenmayayo.reddit
+com.sec.android.app.sbrowser
+com.sec.android.app.sbrowser.beta
+com.shanga.walli
+com.simplehabit.simplehabitapp
+com.slack
+com.snaptube.premium
+com.sololearn
+com.sonelli.juicessh
+com.spotify.music
+com.tencent.huatuo
+com.termux
+com.teslacoilsw.launcher
+com.theinitium.news
+com.thomsonreuters.reuters
+com.thunkable.android.hritvik00.freenom
+com.topjohnwu.magisk
+com.tripadvisor.tripadvisor
+com.tumblr
+com.twitter.android
+com.u91porn
+com.u9porn
+com.ubisoft.dance.justdance2015companion
+com.utopia.pxview
+com.valvesoftware.android.steam.communimunity
+com.valvesoftware.android.steam.community
+com.vanced.android.youtube
+com.vimeo.android.videoapp
+com.vivaldi.browser
+com.vivaldi.browser.snapshot
+com.vkontakte.android
+com.whatsapp
+com.wire
+com.wuxiangai.refactor
+com.xda.labs
+com.xvideos.app
+com.yandex.browser
+com.yandex.browser.beta
+com.yandex.browser.alpha
+com.z28j.feel
+con.medium.reader
+de.apkgrabber
+de.robv.android.xposed.installer
+dk.tacit.android.foldersync.full
+es.rafalense.telegram.themes
+es.rafalense.themes
+flipboard.app
+fm.moon.app
+fr.gouv.etalab.mastodon
+github.tornaco.xposedmoduletest
+idm.internet.download.manager
+idm.internet.download.manager.plus
+io.github.javiewer
+io.github.skyhacker2.magnetsearch
+io.va.exposed
+it.mvilla.android.fenix2
+jp.bokete.app.android
+jp.naver.line.android
+jp.pxv.android
+luo.speedometergpspro
+mark.via.gp
+me.tshine.easymark
+net.teeha.android.url_shortener
+net.tsapps.appsales
+onion.fire
+org.fdroid.fdroid
+org.freedownloadmanager.fdm
+org.kustom.widget
+org.mozilla.fennec_aurora
+org.mozilla.fenix
+org.mozilla.fenix.nightly
+org.mozilla.firefox
+org.mozilla.firefox_beta
+org.mozilla.focus
+org.schabi.newpipe
+org.telegram.messenger
+org.telegram.multi
+org.telegram.plus
+org.thunderdog.challegram
+org.torproject.android
+org.torproject.torbrowser_alpha
+org.wikipedia
+org.xbmc.kodi
+pl.zdunex25.updater
+tv.twitch.android.app
+tw.com.gamer.android.activecenter
+videodownloader.downloadvideo.downloader
+uk.co.bbc.learningenglish
+com.ted.android
diff --git a/V2rayNG/app/src/main/assets/v2ray_config.json b/V2rayNG/app/src/main/assets/v2ray_config.json
new file mode 100644
index 00000000..57b1bd8a
--- /dev/null
+++ b/V2rayNG/app/src/main/assets/v2ray_config.json
@@ -0,0 +1,105 @@
+{
+ "stats":{},
+ "log": {
+ "loglevel": "warning"
+ },
+ "policy":{
+ "levels": {
+ "8": {
+ "handshake": 4,
+ "connIdle": 300,
+ "uplinkOnly": 1,
+ "downlinkOnly": 1
+ }
+ },
+ "system": {
+ "statsOutboundUplink": true,
+ "statsOutboundDownlink": true
+ }
+ },
+ "inbounds": [{
+ "tag": "socks",
+ "port": 10808,
+ "protocol": "socks",
+ "settings": {
+ "auth": "noauth",
+ "udp": true,
+ "userLevel": 8
+ },
+ "sniffing": {
+ "enabled": true,
+ "destOverride": [
+ "http",
+ "tls"
+ ]
+ }
+ },
+ {
+ "tag": "http",
+ "port": 10809,
+ "protocol": "http",
+ "settings": {
+ "userLevel": 8
+ }
+ }
+],
+ "outbounds": [{
+ "tag": "proxy",
+ "protocol": "vmess",
+ "settings": {
+ "vnext": [
+ {
+ "address": "v2ray.cool",
+ "port": 10086,
+ "users": [
+ {
+ "id": "a3482e88-686a-4a58-8126-99c9df64b7bf",
+ "alterId": 0,
+ "security": "auto",
+ "level": 8
+ }
+ ]
+ }
+ ],
+ "servers": [
+ {
+ "address": "v2ray.cool",
+ "method": "chacha20",
+ "ota": false,
+ "password": "123456",
+ "port": 10086,
+ "level": 8
+ }
+ ]
+ },
+ "streamSettings": {
+ "network": "tcp"
+ },
+ "mux": {
+ "enabled": false
+ }
+ },
+ {
+ "protocol": "freedom",
+ "settings": {},
+ "tag": "direct"
+ },
+ {
+ "protocol": "blackhole",
+ "tag": "block",
+ "settings": {
+ "response": {
+ "type": "http"
+ }
+ }
+ }
+ ],
+ "routing": {
+ "domainStrategy": "IPIfNonMatch",
+ "rules": []
+ },
+ "dns": {
+ "hosts": {},
+ "servers": []
+ }
+}
diff --git a/V2rayNG/app/src/main/ic_launcher-web.png b/V2rayNG/app/src/main/ic_launcher-web.png
new file mode 100644
index 00000000..03a4ce8a
Binary files /dev/null and b/V2rayNG/app/src/main/ic_launcher-web.png differ
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/helper/ItemTouchHelperAdapter.java b/V2rayNG/app/src/main/java/com/v2ray/ang/helper/ItemTouchHelperAdapter.java
new file mode 100644
index 00000000..2de0c74a
--- /dev/null
+++ b/V2rayNG/app/src/main/java/com/v2ray/ang/helper/ItemTouchHelperAdapter.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2015 Paul Burke
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.v2ray.ang.helper;
+
+import androidx.recyclerview.widget.RecyclerView;
+import androidx.recyclerview.widget.ItemTouchHelper;
+
+/**
+ * Interface to listen for a move or dismissal event from a {@link ItemTouchHelper.Callback}.
+ *
+ * @author Paul Burke (ipaulpro)
+ */
+public interface ItemTouchHelperAdapter {
+
+ /**
+ * Called when an item has been dragged far enough to trigger a move. This is called every time
+ * an item is shifted, and not at the end of a "drop" event.
+ *
+ * Implementations should call {@link RecyclerView.Adapter#notifyItemMoved(int, int)} after
+ * adjusting the underlying data to reflect this move.
+ *
+ * @param fromPosition The start position of the moved item.
+ * @param toPosition Then resolved position of the moved item.
+ * @return True if the item was moved to the new adapter position.
+ *
+ * @see RecyclerView#getAdapterPositionFor(RecyclerView.ViewHolder)
+ * @see RecyclerView.ViewHolder#getAdapterPosition()
+ */
+ boolean onItemMove(int fromPosition, int toPosition);
+
+
+ void onItemMoveCompleted();
+
+ /**
+ * Called when an item has been dismissed by a swipe.
+ *
+ * Implementations should call {@link RecyclerView.Adapter#notifyItemRemoved(int)} after
+ * adjusting the underlying data to reflect this removal.
+ *
+ * @param position The position of the item dismissed.
+ *
+ * @see RecyclerView#getAdapterPositionFor(RecyclerView.ViewHolder)
+ * @see RecyclerView.ViewHolder#getAdapterPosition()
+ */
+ void onItemDismiss(int position);
+}
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/helper/ItemTouchHelperViewHolder.java b/V2rayNG/app/src/main/java/com/v2ray/ang/helper/ItemTouchHelperViewHolder.java
new file mode 100644
index 00000000..149768fc
--- /dev/null
+++ b/V2rayNG/app/src/main/java/com/v2ray/ang/helper/ItemTouchHelperViewHolder.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2015 Paul Burke
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.v2ray.ang.helper;
+
+import androidx.recyclerview.widget.ItemTouchHelper;
+
+/**
+ * Interface to notify an item ViewHolder of relevant callbacks from {@link
+ * ItemTouchHelper.Callback}.
+ *
+ * @author Paul Burke (ipaulpro)
+ */
+public interface ItemTouchHelperViewHolder {
+
+ /**
+ * Called when the {@link ItemTouchHelper} first registers an item as being moved or swiped.
+ * Implementations should update the item view to indicate it's active state.
+ */
+ void onItemSelected();
+
+
+ /**
+ * Called when the {@link ItemTouchHelper} has completed the move or swipe, and the active item
+ * state should be cleared.
+ */
+ void onItemClear();
+}
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/helper/OnStartDragListener.java b/V2rayNG/app/src/main/java/com/v2ray/ang/helper/OnStartDragListener.java
new file mode 100644
index 00000000..a6407b6f
--- /dev/null
+++ b/V2rayNG/app/src/main/java/com/v2ray/ang/helper/OnStartDragListener.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2015 Paul Burke
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.v2ray.ang.helper;
+
+import androidx.recyclerview.widget.RecyclerView;
+
+/**
+ * Listener for manual initiation of a drag.
+ */
+public interface OnStartDragListener {
+
+ /**
+ * Called when a view is requesting a start of a drag.
+ *
+ * @param viewHolder The holder of the view to drag.
+ */
+ void onStartDrag(RecyclerView.ViewHolder viewHolder);
+
+}
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/helper/SimpleItemTouchHelperCallback.java b/V2rayNG/app/src/main/java/com/v2ray/ang/helper/SimpleItemTouchHelperCallback.java
new file mode 100644
index 00000000..97fceab7
--- /dev/null
+++ b/V2rayNG/app/src/main/java/com/v2ray/ang/helper/SimpleItemTouchHelperCallback.java
@@ -0,0 +1,128 @@
+/*
+ * Copyright (C) 2015 Paul Burke
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.v2ray.ang.helper;
+
+import android.graphics.Canvas;
+import androidx.recyclerview.widget.GridLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+import androidx.recyclerview.widget.ItemTouchHelper;
+
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * An implementation of {@link ItemTouchHelper.Callback} that enables basic drag & drop and
+ * swipe-to-dismiss. Drag events are automatically started by an item long-press.
+ *
+ * Expects the RecyclerView.Adapter
to listen for {@link
+ * ItemTouchHelperAdapter} callbacks and the RecyclerView.ViewHolder
to implement
+ * {@link ItemTouchHelperViewHolder}.
+ *
+ * @author Paul Burke (ipaulpro)
+ */
+public class SimpleItemTouchHelperCallback extends ItemTouchHelper.Callback {
+
+ public static final float ALPHA_FULL = 1.0f;
+
+ private final ItemTouchHelperAdapter mAdapter;
+
+ public SimpleItemTouchHelperCallback(ItemTouchHelperAdapter adapter) {
+ mAdapter = adapter;
+ }
+
+ @Override
+ public boolean isLongPressDragEnabled() {
+ return true;
+ }
+
+ @Override
+ public boolean isItemViewSwipeEnabled() {
+ return false;
+ }
+
+ @Override
+ public int getMovementFlags(RecyclerView recyclerView, @NotNull RecyclerView.ViewHolder viewHolder) {
+ // Set movement flags based on the layout manager
+ if (recyclerView.getLayoutManager() instanceof GridLayoutManager) {
+ final int dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN | ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT;
+ final int swipeFlags = 0;
+ return makeMovementFlags(dragFlags, swipeFlags);
+ } else {
+ final int dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN;
+ final int swipeFlags = ItemTouchHelper.START | ItemTouchHelper.END;
+ return makeMovementFlags(dragFlags, swipeFlags);
+ }
+ }
+
+ @Override
+ public boolean onMove(@NotNull RecyclerView recyclerView, RecyclerView.ViewHolder source, RecyclerView.ViewHolder target) {
+ if (source.getItemViewType() != target.getItemViewType()) {
+ return false;
+ }
+
+ // Notify the adapter of the move
+ mAdapter.onItemMove(source.getBindingAdapterPosition(), target.getBindingAdapterPosition());
+ return true;
+ }
+
+ @Override
+ public void onSwiped(RecyclerView.ViewHolder viewHolder, int i) {
+ // Notify the adapter of the dismissal
+ mAdapter.onItemDismiss(viewHolder.getBindingAdapterPosition());
+ }
+
+ @Override
+ public void onChildDraw(@NotNull Canvas c, @NotNull RecyclerView recyclerView, @NotNull RecyclerView.ViewHolder viewHolder, float dX,
+ float dY, int actionState, boolean isCurrentlyActive) {
+ if (actionState == ItemTouchHelper.ACTION_STATE_SWIPE) {
+ // Fade out the view as it is swiped out of the parent's bounds
+ final float alpha = ALPHA_FULL - Math.abs(dX) / (float) viewHolder.itemView.getWidth();
+ viewHolder.itemView.setAlpha(alpha);
+ viewHolder.itemView.setTranslationX(dX);
+ } else {
+ super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive);
+ }
+ }
+
+ @Override
+ public void onSelectedChanged(RecyclerView.ViewHolder viewHolder, int actionState) {
+ // We only want the active item to change
+ if (actionState != ItemTouchHelper.ACTION_STATE_IDLE) {
+ if (viewHolder instanceof ItemTouchHelperViewHolder) {
+ // Let the view holder know that this item is being moved or dragged
+ ItemTouchHelperViewHolder itemViewHolder = (ItemTouchHelperViewHolder) viewHolder;
+ itemViewHolder.onItemSelected();
+ }
+ }
+
+ super.onSelectedChanged(viewHolder, actionState);
+ }
+
+ @Override
+ public void clearView(@NotNull RecyclerView recyclerView, @NotNull RecyclerView.ViewHolder viewHolder) {
+ super.clearView(recyclerView, viewHolder);
+
+ mAdapter.onItemMoveCompleted();
+
+ viewHolder.itemView.setAlpha(ALPHA_FULL);
+
+ if (viewHolder instanceof ItemTouchHelperViewHolder) {
+ // Tell the view holder it's time to restore the idle state
+ ItemTouchHelperViewHolder itemViewHolder = (ItemTouchHelperViewHolder) viewHolder;
+ itemViewHolder.onItemClear();
+ }
+ }
+}
diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/AngApplication.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/AngApplication.kt
new file mode 100644
index 00000000..aa1ef7d4
--- /dev/null
+++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/AngApplication.kt
@@ -0,0 +1,28 @@
+package com.v2ray.ang
+
+import androidx.multidex.MultiDexApplication
+import androidx.preference.PreferenceManager
+import com.tencent.mmkv.MMKV
+
+class AngApplication : MultiDexApplication() {
+ companion object {
+ const val PREF_LAST_VERSION = "pref_last_version"
+ }
+
+ var firstRun = false
+ private set
+
+ override fun onCreate() {
+ super.onCreate()
+
+// LeakCanary.install(this)
+
+ val defaultSharedPreferences = PreferenceManager.getDefaultSharedPreferences(this)
+ firstRun = defaultSharedPreferences.getInt(PREF_LAST_VERSION, 0) != BuildConfig.VERSION_CODE
+ if (firstRun)
+ defaultSharedPreferences.edit().putInt(PREF_LAST_VERSION, BuildConfig.VERSION_CODE).apply()
+
+ //Logger.init().logLevel(if (BuildConfig.DEBUG) LogLevel.FULL else LogLevel.NONE)
+ MMKV.initialize(this)
+ }
+}
diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/AppConfig.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/AppConfig.kt
new file mode 100644
index 00000000..3793e583
--- /dev/null
+++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/AppConfig.kt
@@ -0,0 +1,89 @@
+package com.v2ray.ang
+
+/**
+ *
+ * App Config Const
+ */
+object AppConfig {
+ const val ANG_PACKAGE = "com.v2ray.ang"
+ const val DIR_ASSETS = "assets"
+
+ // legacy
+ const val ANG_CONFIG = "ang_config"
+ const val PREF_INAPP_BUY_IS_PREMIUM = "pref_inapp_buy_is_premium"
+ const val PREF_ROUTING_CUSTOM = "pref_routing_custom"
+
+ // Preferences mapped to MMKV
+ const val PREF_MODE = "pref_mode"
+ const val PREF_SPEED_ENABLED = "pref_speed_enabled"
+ const val PREF_SNIFFING_ENABLED = "pref_sniffing_enabled"
+ const val PREF_PROXY_SHARING = "pref_proxy_sharing_enabled"
+ const val PREF_LOCAL_DNS_ENABLED = "pref_local_dns_enabled"
+ const val PREF_FAKE_DNS_ENABLED = "pref_fake_dns_enabled"
+ const val PREF_VPN_DNS = "pref_vpn_dns"
+ const val PREF_REMOTE_DNS = "pref_remote_dns"
+ const val PREF_DOMESTIC_DNS = "pref_domestic_dns"
+ const val PREF_LOCAL_DNS_PORT = "pref_local_dns_port"
+ const val PREF_ALLOW_INSECURE = "pref_allow_insecure"
+ const val PREF_SOCKS_PORT = "pref_socks_port"
+ const val PREF_HTTP_PORT = "pref_http_port"
+ const val PREF_LOGLEVEL = "pref_core_loglevel"
+ const val PREF_LANGUAGE = "pref_language"
+ const val PREF_PREFER_IPV6 = "pref_prefer_ipv6"
+ const val PREF_ROUTING_DOMAIN_STRATEGY = "pref_routing_domain_strategy"
+ const val PREF_ROUTING_MODE = "pref_routing_mode"
+ const val PREF_V2RAY_ROUTING_AGENT = "pref_v2ray_routing_agent"
+ const val PREF_V2RAY_ROUTING_DIRECT = "pref_v2ray_routing_direct"
+ const val PREF_V2RAY_ROUTING_BLOCKED = "pref_v2ray_routing_blocked"
+ const val PREF_PER_APP_PROXY = "pref_per_app_proxy"
+ const val PREF_PER_APP_PROXY_SET = "pref_per_app_proxy_set"
+ const val PREF_BYPASS_APPS = "pref_bypass_apps"
+ const val PREF_CONFIRM_REMOVE = "pref_confirm_remove"
+
+ const val HTTP_PROTOCOL: String = "http://"
+ const val HTTPS_PROTOCOL: String = "https://"
+
+ const val BROADCAST_ACTION_SERVICE = "com.v2ray.ang.action.service"
+ const val BROADCAST_ACTION_ACTIVITY = "com.v2ray.ang.action.activity"
+ const val BROADCAST_ACTION_WIDGET_CLICK = "com.v2ray.ang.action.widget.click"
+
+ const val TASKER_EXTRA_BUNDLE = "com.twofortyfouram.locale.intent.extra.BUNDLE"
+ const val TASKER_EXTRA_STRING_BLURB = "com.twofortyfouram.locale.intent.extra.BLURB"
+ const val TASKER_EXTRA_BUNDLE_SWITCH = "tasker_extra_bundle_switch"
+ const val TASKER_EXTRA_BUNDLE_GUID = "tasker_extra_bundle_guid"
+ const val TASKER_DEFAULT_GUID = "Default"
+
+ const val TAG_AGENT = "proxy"
+ const val TAG_DIRECT = "direct"
+ const val TAG_BLOCKED = "block"
+
+ const val androidpackagenamelistUrl = "https://raw.githubusercontent.com/2dust/androidpackagenamelist/master/proxy.txt"
+ const val v2rayCustomRoutingListUrl = "https://raw.githubusercontent.com/2dust/v2rayCustomRoutingList/master/"
+ const val v2rayNGIssues = "https://github.com/2dust/v2rayNG/issues"
+ const val v2rayNGWikiMode = "https://github.com/2dust/v2rayNG/wiki/Mode"
+ const val promotionUrl = "aHR0cHM6Ly85LjIzNDQ1Ni54eXovYWJjLmh0bWw="
+ const val geoUrl = "https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/";
+
+ const val DNS_AGENT = "1.1.1.1"
+ const val DNS_DIRECT = "223.5.5.5"
+
+ const val PORT_LOCAL_DNS = "10853"
+ const val PORT_SOCKS = "10808"
+ const val PORT_HTTP = "10809"
+
+ const val MSG_REGISTER_CLIENT = 1
+ const val MSG_STATE_RUNNING = 11
+ const val MSG_STATE_NOT_RUNNING = 12
+ const val MSG_UNREGISTER_CLIENT = 2
+ const val MSG_STATE_START = 3
+ const val MSG_STATE_START_SUCCESS = 31
+ const val MSG_STATE_START_FAILURE = 32
+ const val MSG_STATE_STOP = 4
+ const val MSG_STATE_STOP_SUCCESS = 41
+ const val MSG_STATE_RESTART = 5
+ const val MSG_MEASURE_DELAY = 6
+ const val MSG_MEASURE_DELAY_SUCCESS = 61
+ const val MSG_MEASURE_CONFIG = 7
+ const val MSG_MEASURE_CONFIG_SUCCESS = 71
+ const val MSG_MEASURE_CONFIG_CANCEL = 72
+}
diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/AngConfig.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/AngConfig.kt
new file mode 100644
index 00000000..01f2bdd8
--- /dev/null
+++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/AngConfig.kt
@@ -0,0 +1,32 @@
+package com.v2ray.ang.dto
+
+data class AngConfig(
+ var index: Int,
+ var vmess: ArrayList,
+ var subItem: ArrayList
+) {
+ data class VmessBean(var guid: String = "123456",
+ var address: String = "v2ray.cool",
+ var port: Int = 10086,
+ var id: String = "a3482e88-686a-4a58-8126-99c9df64b7bf",
+ var alterId: Int = 64,
+ var security: String = "aes-128-cfb",
+ var network: String = "tcp",
+ var remarks: String = "def",
+ var headerType: String = "",
+ var requestHost: String = "",
+ var path: String = "",
+ var streamSecurity: String = "",
+ var allowInsecure: String = "",
+ var configType: Int = 1,
+ var configVersion: Int = 1,
+ var testResult: String = "",
+ var subid: String = "",
+ var flow: String = "",
+ var sni: String = "")
+
+ data class SubItemBean(var id: String = "",
+ var remarks: String = "",
+ var url: String = "",
+ var enabled: Boolean = true)
+}
diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/AppInfo.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/AppInfo.kt
new file mode 100644
index 00000000..f99655a8
--- /dev/null
+++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/AppInfo.kt
@@ -0,0 +1,9 @@
+package com.v2ray.ang.dto
+
+import android.graphics.drawable.Drawable
+
+data class AppInfo(val appName: String,
+ val packageName: String,
+ val appIcon: Drawable,
+ val isSystemApp: Boolean,
+ var isSelected: Int)
\ No newline at end of file
diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/EConfigType.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/EConfigType.kt
new file mode 100644
index 00000000..5204c0ca
--- /dev/null
+++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/EConfigType.kt
@@ -0,0 +1,15 @@
+package com.v2ray.ang.dto
+
+enum class EConfigType(val value: Int, val protocolScheme: String) {
+ VMESS(1, "vmess://"),
+ CUSTOM(2, ""),
+ SHADOWSOCKS(3, "ss://"),
+ SOCKS(4, "socks://"),
+ VLESS(5, "vless://"),
+ TROJAN(6, "trojan://"),
+ WIREGUARD(7, "wireguard://");
+
+ companion object {
+ fun fromInt(value: Int) = values().firstOrNull { it.value == value }
+ }
+}
diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/ERoutingMode.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/ERoutingMode.kt
new file mode 100644
index 00000000..96d77397
--- /dev/null
+++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/ERoutingMode.kt
@@ -0,0 +1,9 @@
+package com.v2ray.ang.dto
+
+enum class ERoutingMode(val value: String ) {
+ GLOBAL_PROXY("0"),
+ BYPASS_LAN("1"),
+ BYPASS_MAINLAND("2"),
+ BYPASS_LAN_MAINLAND("3"),
+ GLOBAL_DIRECT("4");
+}
diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/ServerAffiliationInfo.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/ServerAffiliationInfo.kt
new file mode 100644
index 00000000..bb4d20be
--- /dev/null
+++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/ServerAffiliationInfo.kt
@@ -0,0 +1,10 @@
+package com.v2ray.ang.dto
+
+data class ServerAffiliationInfo(var testDelayMillis: Long = 0L) {
+ fun getTestDelayString(): String {
+ if (testDelayMillis == 0L) {
+ return ""
+ }
+ return testDelayMillis.toString() + "ms"
+ }
+}
diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/ServerConfig.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/ServerConfig.kt
new file mode 100644
index 00000000..88a9a7ec
--- /dev/null
+++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/ServerConfig.kt
@@ -0,0 +1,69 @@
+package com.v2ray.ang.dto
+
+import com.v2ray.ang.AppConfig.TAG_AGENT
+import com.v2ray.ang.AppConfig.TAG_BLOCKED
+import com.v2ray.ang.AppConfig.TAG_DIRECT
+import com.v2ray.ang.util.Utils
+
+data class ServerConfig(
+ val configVersion: Int = 3,
+ val configType: EConfigType,
+ var subscriptionId: String = "",
+ val addedTime: Long = System.currentTimeMillis(),
+ var remarks: String = "",
+ val outboundBean: V2rayConfig.OutboundBean? = null,
+ var fullConfig: V2rayConfig? = null
+) {
+ companion object {
+ fun create(configType: EConfigType): ServerConfig {
+ when(configType) {
+ EConfigType.VMESS, EConfigType.VLESS ->
+ return ServerConfig(
+ configType = configType,
+ outboundBean = V2rayConfig.OutboundBean(
+ protocol = configType.name.lowercase(),
+ settings = V2rayConfig.OutboundBean.OutSettingsBean(
+ vnext = listOf(V2rayConfig.OutboundBean.OutSettingsBean.VnextBean(
+ users = listOf(V2rayConfig.OutboundBean.OutSettingsBean.VnextBean.UsersBean())))),
+ streamSettings = V2rayConfig.OutboundBean.StreamSettingsBean()))
+ EConfigType.CUSTOM, EConfigType.WIREGUARD ->
+ return ServerConfig(configType = configType)
+ EConfigType.SHADOWSOCKS, EConfigType.SOCKS, EConfigType.TROJAN ->
+ return ServerConfig(
+ configType = configType,
+ outboundBean = V2rayConfig.OutboundBean(
+ protocol = configType.name.lowercase(),
+ settings = V2rayConfig.OutboundBean.OutSettingsBean(
+ servers = listOf(V2rayConfig.OutboundBean.OutSettingsBean.ServersBean())),
+ streamSettings = V2rayConfig.OutboundBean.StreamSettingsBean()))
+ }
+ }
+ }
+
+ fun getProxyOutbound(): V2rayConfig.OutboundBean? {
+ if (configType != EConfigType.CUSTOM) {
+ return outboundBean
+ }
+ return fullConfig?.getProxyOutbound()
+ }
+
+ fun getAllOutboundTags(): MutableList {
+ if (configType != EConfigType.CUSTOM) {
+ return mutableListOf(TAG_AGENT, TAG_DIRECT, TAG_BLOCKED)
+ }
+ fullConfig?.let { config ->
+ return config.outbounds.map { it.tag }.toMutableList()
+ }
+ return mutableListOf()
+ }
+
+ fun getV2rayPointDomainAndPort(): String {
+ val address = getProxyOutbound()?.getServerAddress().orEmpty()
+ val port = getProxyOutbound()?.getServerPort()
+ return if (Utils.isIpv6Address(address)) {
+ String.format("[%s]:%s", address, port)
+ } else {
+ String.format("%s:%s", address, port)
+ }
+ }
+}
diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/ServersCache.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/ServersCache.kt
new file mode 100644
index 00000000..e686b469
--- /dev/null
+++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/ServersCache.kt
@@ -0,0 +1,4 @@
+package com.v2ray.ang.dto
+
+data class ServersCache(val guid: String,
+ val config: ServerConfig)
\ No newline at end of file
diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/SubscriptionItem.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/SubscriptionItem.kt
new file mode 100644
index 00000000..b2195148
--- /dev/null
+++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/SubscriptionItem.kt
@@ -0,0 +1,8 @@
+package com.v2ray.ang.dto
+
+data class SubscriptionItem(
+ var remarks: String = "",
+ var url: String = "",
+ var enabled: Boolean = true,
+ val addedTime: Long = System.currentTimeMillis()) {
+}
diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/V2rayConfig.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/V2rayConfig.kt
new file mode 100644
index 00000000..cfc771b1
--- /dev/null
+++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/V2rayConfig.kt
@@ -0,0 +1,462 @@
+package com.v2ray.ang.dto
+
+import android.text.TextUtils
+import com.google.gson.GsonBuilder
+import com.google.gson.JsonPrimitive
+import com.google.gson.JsonSerializationContext
+import com.google.gson.JsonSerializer
+import com.google.gson.annotations.SerializedName
+import com.google.gson.reflect.TypeToken
+import java.lang.reflect.Type
+
+data class V2rayConfig(
+ var stats: Any? = null,
+ val log: LogBean,
+ var policy: PolicyBean?,
+ val inbounds: ArrayList,
+ var outbounds: ArrayList,
+ var dns: DnsBean,
+ val routing: RoutingBean,
+ val api: Any? = null,
+ val transport: Any? = null,
+ val reverse: Any? = null,
+ var fakedns: Any? = null,
+ val browserForwarder: Any? = null) {
+ companion object {
+ const val DEFAULT_PORT = 443
+ const val DEFAULT_SECURITY = "auto"
+ const val DEFAULT_LEVEL = 8
+ const val DEFAULT_NETWORK = "tcp"
+
+ const val TLS = "tls"
+ const val XTLS = "xtls"
+ const val HTTP = "http"
+ }
+
+ data class LogBean(val access: String,
+ val error: String,
+ var loglevel: String?,
+ val dnsLog: Boolean? = null)
+
+ data class InboundBean(
+ var tag: String,
+ var port: Int,
+ var protocol: String,
+ var listen: String? = null,
+ val settings: Any? = null,
+ val sniffing: SniffingBean?,
+ val streamSettings: Any? = null,
+ val allocate: Any? = null) {
+
+ data class InSettingsBean(val auth: String? = null,
+ val udp: Boolean? = null,
+ val userLevel: Int? = null,
+ val address: String? = null,
+ val port: Int? = null,
+ val network: String? = null)
+
+ data class SniffingBean(var enabled: Boolean,
+ val destOverride: ArrayList,
+ val metadataOnly: Boolean? = null)
+ }
+
+ data class OutboundBean(val tag: String = "proxy",
+ var protocol: String,
+ var settings: OutSettingsBean? = null,
+ var streamSettings: StreamSettingsBean? = null,
+ val proxySettings: Any? = null,
+ val sendThrough: String? = null,
+ val mux: MuxBean? = MuxBean(false)) {
+
+ data class OutSettingsBean(var vnext: List? = null,
+ var servers: List? = null,
+ /*Blackhole*/
+ var response: Response? = null,
+ /*DNS*/
+ val network: String? = null,
+ val address: Any? = null,
+ val port: Int? = null,
+ /*Freedom*/
+ var domainStrategy: String? = null,
+ val redirect: String? = null,
+ val userLevel: Int? = null,
+ /*Loopback*/
+ val inboundTag: String? = null,
+ /*Wireguard*/
+ val secretKey: String? = null,
+ val peers: List? = null,
+ ) {
+
+ data class VnextBean(var address: String = "",
+ var port: Int = DEFAULT_PORT,
+ var users: List) {
+
+ data class UsersBean(var id: String = "",
+ var alterId: Int? = null,
+ var security: String = DEFAULT_SECURITY,
+ var level: Int = DEFAULT_LEVEL,
+ var encryption: String = "",
+ var flow: String = "")
+ }
+
+ data class ServersBean(var address: String = "",
+ var method: String = "chacha20-poly1305",
+ var ota: Boolean = false,
+ var password: String = "",
+ var port: Int = DEFAULT_PORT,
+ var level: Int = DEFAULT_LEVEL,
+ val email: String? = null,
+ var flow: String? = null,
+ val ivCheck: Boolean? = null,
+ var users: List? = null) {
+
+
+ data class SocksUsersBean(var user: String = "",
+ var pass: String = "",
+ var level: Int = DEFAULT_LEVEL)
+ }
+
+ data class Response(var type: String)
+
+ data class WireGuardBean(var publicKey: String = "",
+ var endpoint: String = "")
+ }
+
+ data class StreamSettingsBean(var network: String = DEFAULT_NETWORK,
+ var security: String = "",
+ var tcpSettings: TcpSettingsBean? = null,
+ var kcpSettings: KcpSettingsBean? = null,
+ var wsSettings: WsSettingsBean? = null,
+ var httpSettings: HttpSettingsBean? = null,
+ var tlsSettings: TlsSettingsBean? = null,
+ var quicSettings: QuicSettingBean? = null,
+ var xtlsSettings: TlsSettingsBean? = null,
+ var grpcSettings: GrpcSettingsBean? = null,
+ val dsSettings: Any? = null,
+ val sockopt: Any? = null
+ ) {
+
+ data class TcpSettingsBean(var header: HeaderBean = HeaderBean(),
+ val acceptProxyProtocol: Boolean? = null) {
+ data class HeaderBean(var type: String = "none",
+ var request: RequestBean? = null,
+ var response: Any? = null) {
+ data class RequestBean(var path: List = ArrayList(),
+ var headers: HeadersBean = HeadersBean(),
+ val version: String? = null,
+ val method: String? = null) {
+ data class HeadersBean(var Host: List = ArrayList(),
+ @SerializedName("User-Agent")
+ val userAgent: List? = null,
+ @SerializedName("Accept-Encoding")
+ val acceptEncoding: List? = null,
+ val Connection: List? = null,
+ val Pragma: String? = null)
+ }
+ }
+ }
+
+ data class KcpSettingsBean(var mtu: Int = 1350,
+ var tti: Int = 50,
+ var uplinkCapacity: Int = 12,
+ var downlinkCapacity: Int = 100,
+ var congestion: Boolean = false,
+ var readBufferSize: Int = 1,
+ var writeBufferSize: Int = 1,
+ var header: HeaderBean = HeaderBean(),
+ var seed: String? = null) {
+ data class HeaderBean(var type: String = "none")
+ }
+
+ data class WsSettingsBean(var path: String = "",
+ var headers: HeadersBean = HeadersBean(),
+ val maxEarlyData: Int? = null,
+ val useBrowserForwarding: Boolean? = null,
+ val acceptProxyProtocol: Boolean? = null) {
+ data class HeadersBean(var Host: String = "")
+ }
+
+ data class HttpSettingsBean(var host: List = ArrayList(),
+ var path: String = "")
+
+ data class TlsSettingsBean(var allowInsecure: Boolean = false,
+ var serverName: String = "",
+ val alpn: List? = null,
+ val minVersion: String? = null,
+ val maxVersion: String? = null,
+ val preferServerCipherSuites: Boolean? = null,
+ val cipherSuites: String? = null,
+ val fingerprint: String? = null,
+ val certificates: List? = null,
+ val disableSystemRoot: Boolean? = null,
+ val enableSessionResumption: Boolean? = null)
+
+ data class QuicSettingBean(var security: String = "none",
+ var key: String = "",
+ var header: HeaderBean = HeaderBean()) {
+ data class HeaderBean(var type: String = "none")
+ }
+
+ data class GrpcSettingsBean(var serviceName: String = "",
+ var multiMode: Boolean? = null)
+
+ fun populateTransportSettings(transport: String, headerType: String?, host: String?, path: String?, seed: String?,
+ quicSecurity: String?, key: String?, mode: String?, serviceName: String?): String {
+ var sni = ""
+ network = transport
+ when (network) {
+ "tcp" -> {
+ val tcpSetting = TcpSettingsBean()
+ if (headerType == HTTP) {
+ tcpSetting.header.type = HTTP
+ if (!TextUtils.isEmpty(host) || !TextUtils.isEmpty(path)) {
+ val requestObj = TcpSettingsBean.HeaderBean.RequestBean()
+ requestObj.headers.Host = (host ?: "").split(",").map { it.trim() }.filter { it.isNotEmpty() }
+ requestObj.path = (path ?: "").split(",").map { it.trim() }.filter { it.isNotEmpty() }
+ tcpSetting.header.request = requestObj
+ sni = requestObj.headers.Host.getOrNull(0) ?: sni
+ }
+ } else {
+ tcpSetting.header.type = "none"
+ sni = host ?: ""
+ }
+ tcpSettings = tcpSetting
+ }
+ "kcp" -> {
+ val kcpsetting = KcpSettingsBean()
+ kcpsetting.header.type = headerType ?: "none"
+ if (seed.isNullOrEmpty()) {
+ kcpsetting.seed = null
+ } else {
+ kcpsetting.seed = seed
+ }
+ kcpSettings = kcpsetting
+ }
+ "ws" -> {
+ val wssetting = WsSettingsBean()
+ wssetting.headers.Host = host ?: ""
+ sni = wssetting.headers.Host
+ wssetting.path = path ?: "/"
+ wsSettings = wssetting
+ }
+ "h2", "http" -> {
+ network = "h2"
+ val h2Setting = HttpSettingsBean()
+ h2Setting.host = (host ?: "").split(",").map { it.trim() }.filter { it.isNotEmpty() }
+ sni = h2Setting.host.getOrNull(0) ?: sni
+ h2Setting.path = path ?: "/"
+ httpSettings = h2Setting
+ }
+ "quic" -> {
+ val quicsetting = QuicSettingBean()
+ quicsetting.security = quicSecurity ?: "none"
+ quicsetting.key = key ?: ""
+ quicsetting.header.type = headerType ?: "none"
+ quicSettings = quicsetting
+ }
+ "grpc" -> {
+ val grpcSetting = GrpcSettingsBean()
+ grpcSetting.multiMode = mode == "multi"
+ grpcSetting.serviceName = serviceName ?: ""
+ sni = host ?: ""
+ grpcSettings = grpcSetting
+ }
+ }
+ return sni
+ }
+
+ fun populateTlsSettings(streamSecurity: String, allowInsecure: Boolean, sni: String, fingerprint: String?, alpns: String?) {
+ security = streamSecurity
+ val tlsSetting = TlsSettingsBean(
+ allowInsecure = allowInsecure,
+ serverName = sni,
+ fingerprint = fingerprint,
+ alpn = if (alpns.isNullOrEmpty()) null else alpns.split(",").map { it.trim() }.filter { it.isNotEmpty() }
+ )
+ if (security == TLS) {
+ tlsSettings = tlsSetting
+ xtlsSettings = null
+ } else if (security == XTLS) {
+ tlsSettings = null
+ xtlsSettings = tlsSetting
+ }
+ }
+ }
+
+ data class MuxBean(var enabled: Boolean, var concurrency: Int = 8)
+
+ fun getServerAddress(): String? {
+ if (protocol.equals(EConfigType.VMESS.name, true)
+ || protocol.equals(EConfigType.VLESS.name, true)) {
+ return settings?.vnext?.get(0)?.address
+ } else if (protocol.equals(EConfigType.SHADOWSOCKS.name, true)
+ || protocol.equals(EConfigType.SOCKS.name, true)
+ || protocol.equals(EConfigType.TROJAN.name, true)) {
+ return settings?.servers?.get(0)?.address
+ } else if (protocol.equals(EConfigType.WIREGUARD.name, true)) {
+ return settings?.peers?.get(0)?.endpoint?.substringBeforeLast(":")
+ }
+ return null
+ }
+
+ fun getServerPort(): Int? {
+ if (protocol.equals(EConfigType.VMESS.name, true)
+ || protocol.equals(EConfigType.VLESS.name, true)) {
+ return settings?.vnext?.get(0)?.port
+ } else if (protocol.equals(EConfigType.SHADOWSOCKS.name, true)
+ || protocol.equals(EConfigType.SOCKS.name, true)
+ || protocol.equals(EConfigType.TROJAN.name, true)) {
+ return settings?.servers?.get(0)?.port
+ } else if (protocol.equals(EConfigType.WIREGUARD.name, true)) {
+ return settings?.peers?.get(0)?.endpoint?.substringAfterLast(":")?.toInt()
+ }
+ return null
+ }
+
+ fun getPassword(): String? {
+ if (protocol.equals(EConfigType.VMESS.name, true)
+ || protocol.equals(EConfigType.VLESS.name, true)) {
+ return settings?.vnext?.get(0)?.users?.get(0)?.id
+ } else if (protocol.equals(EConfigType.SHADOWSOCKS.name, true)
+ || protocol.equals(EConfigType.TROJAN.name, true)) {
+ return settings?.servers?.get(0)?.password
+ } else if (protocol.equals(EConfigType.SOCKS.name, true)) {
+ return settings?.servers?.get(0)?.users?.get(0)?.pass
+ } else if (protocol.equals(EConfigType.WIREGUARD.name, true)) {
+ return settings?.secretKey
+ }
+ return null
+ }
+
+ fun getSecurityEncryption(): String? {
+ return when {
+ protocol.equals(EConfigType.VMESS.name, true) -> settings?.vnext?.get(0)?.users?.get(0)?.security
+ protocol.equals(EConfigType.VLESS.name, true) -> settings?.vnext?.get(0)?.users?.get(0)?.encryption
+ protocol.equals(EConfigType.SHADOWSOCKS.name, true) -> settings?.servers?.get(0)?.method
+ else -> null
+ }
+ }
+
+ fun getTransportSettingDetails(): List? {
+ if (protocol.equals(EConfigType.VMESS.name, true)
+ || protocol.equals(EConfigType.VLESS.name, true)
+ || protocol.equals(EConfigType.TROJAN.name, true)) {
+ val transport = streamSettings?.network ?: return null
+ return when (transport) {
+ "tcp" -> {
+ val tcpSetting = streamSettings?.tcpSettings ?: return null
+ listOf(tcpSetting.header.type,
+ tcpSetting.header.request?.headers?.Host?.joinToString().orEmpty(),
+ tcpSetting.header.request?.path?.joinToString().orEmpty())
+ }
+ "kcp" -> {
+ val kcpSetting = streamSettings?.kcpSettings ?: return null
+ listOf(kcpSetting.header.type,
+ "",
+ kcpSetting.seed.orEmpty())
+ }
+ "ws" -> {
+ val wsSetting = streamSettings?.wsSettings ?: return null
+ listOf("",
+ wsSetting.headers.Host,
+ wsSetting.path)
+ }
+ "h2" -> {
+ val h2Setting = streamSettings?.httpSettings ?: return null
+ listOf("",
+ h2Setting.host.joinToString(),
+ h2Setting.path)
+ }
+ "quic" -> {
+ val quicSetting = streamSettings?.quicSettings ?: return null
+ listOf(quicSetting.header.type,
+ quicSetting.security,
+ quicSetting.key)
+ }
+ "grpc" -> {
+ val grpcSetting = streamSettings?.grpcSettings ?: return null
+ listOf(if (grpcSetting.multiMode == true) "multi" else "gun",
+ "",
+ grpcSetting.serviceName)
+ }
+ else -> null
+ }
+ }
+ return null
+ }
+ }
+
+ data class DnsBean(var servers: ArrayList? = null,
+ var hosts: Map? = null,
+ val clientIp: String? = null,
+ val disableCache: Boolean? = null,
+ val queryStrategy: String? = null,
+ val tag: String? = null
+ ) {
+ data class ServersBean(var address: String = "",
+ var port: Int? = null,
+ var domains: List? = null,
+ var expectIPs: List? = null,
+ val clientIp: String? = null)
+ }
+
+ data class RoutingBean(var domainStrategy: String,
+ var domainMatcher: String? = null,
+ var rules: ArrayList,
+ val balancers: List? = null) {
+
+ data class RulesBean(var type: String = "",
+ var ip: ArrayList? = null,
+ var domain: ArrayList? = null,
+ var outboundTag: String = "",
+ var balancerTag: String? = null,
+ var port: String? = null,
+ val sourcePort: String? = null,
+ val network: String? = null,
+ val source: List? = null,
+ val user: List? = null,
+ var inboundTag: List? = null,
+ val protocol: List? = null,
+ val attrs: String? = null,
+ val domainMatcher: String? = null
+ )
+ }
+
+ data class PolicyBean(var levels: Map,
+ var system: Any? = null) {
+ data class LevelBean(
+ var handshake: Int? = null,
+ var connIdle: Int? = null,
+ var uplinkOnly: Int? = null,
+ var downlinkOnly: Int? = null,
+ val statsUserUplink: Boolean? = null,
+ val statsUserDownlink: Boolean? = null,
+ var bufferSize: Int? = null)
+ }
+
+ data class FakednsBean(var ipPool: String = "198.18.0.0/15",
+ var poolSize: Int = 10000) // roughly 10 times smaller than total ip pool
+
+ fun getProxyOutbound(): OutboundBean? {
+ outbounds.forEach { outbound ->
+ EConfigType.values().forEach {
+ if (outbound.protocol.equals(it.name, true)) {
+ return outbound
+ }
+ }
+ }
+ return null
+ }
+
+ fun toPrettyPrinting(): String {
+ return GsonBuilder()
+ .setPrettyPrinting()
+ .disableHtmlEscaping()
+ .registerTypeAdapter( // custom serialiser is needed here since JSON by default parse number as Double, core will fail to start
+ object : TypeToken() {}.type,
+ JsonSerializer { src: Double?, _: Type?, _: JsonSerializationContext? -> JsonPrimitive(src?.toInt()) }
+ )
+ .create()
+ .toJson(this)
+ }
+}
diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/VmessQRCode.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/VmessQRCode.kt
new file mode 100644
index 00000000..48f3a341
--- /dev/null
+++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/VmessQRCode.kt
@@ -0,0 +1,16 @@
+package com.v2ray.ang.dto
+
+data class VmessQRCode(var v: String = "",
+ var ps: String = "",
+ var add: String = "",
+ var port: String = "",
+ var id: String = "",
+ var aid: String = "0",
+ var scy: String = "",
+ var net: String = "",
+ var type: String = "",
+ var host: String = "",
+ var path: String = "",
+ var tls: String = "",
+ var sni: String = "",
+ var alpn: String = "")
\ No newline at end of file
diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/extension/_Ext.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/extension/_Ext.kt
new file mode 100644
index 00000000..fb0bff63
--- /dev/null
+++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/extension/_Ext.kt
@@ -0,0 +1,80 @@
+package com.v2ray.ang.extension
+
+import android.content.Context
+import android.os.Build
+import android.widget.Toast
+import com.v2ray.ang.AngApplication
+import me.drakeet.support.toast.ToastCompat
+import org.json.JSONObject
+import java.net.URI
+import java.net.URLConnection
+
+/**
+ * Some extensions
+ */
+
+val Context.v2RayApplication: AngApplication
+ get() = applicationContext as AngApplication
+
+fun Context.toast(message: Int): Toast = ToastCompat
+ .makeText(this, message, Toast.LENGTH_SHORT)
+ .apply {
+ show()
+ }
+
+fun Context.toast(message: CharSequence): Toast = ToastCompat
+ .makeText(this, message, Toast.LENGTH_SHORT)
+ .apply {
+ show()
+ }
+
+fun JSONObject.putOpt(pair: Pair) = putOpt(pair.first, pair.second)
+fun JSONObject.putOpt(pairs: Map) = pairs.forEach { putOpt(it.key to it.value) }
+
+const val threshold = 1000
+const val divisor = 1024F
+
+fun Long.toSpeedString() = toTrafficString() + "/s"
+
+fun Long.toTrafficString(): String {
+ if (this == 0L)
+ return "\t\t\t0\t B"
+
+ if (this < threshold)
+ return "${this.toFloat().toShortString()}\t B"
+
+ val kib = this / divisor
+ if (kib < threshold)
+ return "${kib.toShortString()}\t KB"
+
+ val mib = kib / divisor
+ if (mib < threshold)
+ return "${mib.toShortString()}\t MB"
+
+ val gib = mib / divisor
+ if (gib < threshold)
+ return "${gib.toShortString()}\t GB"
+
+ val tib = gib / divisor
+ if (tib < threshold)
+ return "${tib.toShortString()}\t TB"
+
+ val pib = tib / divisor
+ if (pib < threshold)
+ return "${pib.toShortString()}\t PB"
+
+ return "∞"
+}
+
+private fun Float.toShortString(): String {
+ val s = "%.2f".format(this)
+ if (s.length <= 4)
+ return s
+ return s.substring(0, 4).removeSuffix(".")
+}
+
+val URLConnection.responseLength: Long
+ get() = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) contentLengthLong else contentLength.toLong()
+
+val URI.idnHost: String
+ get() = (host!!).replace("[", "").replace("]", "")
\ No newline at end of file
diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/receiver/TaskerReceiver.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/receiver/TaskerReceiver.kt
new file mode 100644
index 00000000..08a3184a
--- /dev/null
+++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/receiver/TaskerReceiver.kt
@@ -0,0 +1,41 @@
+package com.v2ray.ang.receiver
+
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.text.TextUtils
+import com.google.zxing.WriterException
+import com.tencent.mmkv.MMKV
+import com.v2ray.ang.AppConfig
+import com.v2ray.ang.service.V2RayServiceManager
+import com.v2ray.ang.util.MmkvManager
+
+import com.v2ray.ang.util.Utils
+
+class TaskerReceiver : BroadcastReceiver() {
+ private val mainStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_MAIN, MMKV.MULTI_PROCESS_MODE) }
+
+ override fun onReceive(context: Context, intent: Intent?) {
+
+ try {
+ val bundle = intent?.getBundleExtra(AppConfig.TASKER_EXTRA_BUNDLE)
+ val switch = bundle?.getBoolean(AppConfig.TASKER_EXTRA_BUNDLE_SWITCH, false)
+ val guid = bundle?.getString(AppConfig.TASKER_EXTRA_BUNDLE_GUID, "")
+
+ if (switch == null || guid == null || TextUtils.isEmpty(guid)) {
+ return
+ } else if (switch) {
+ if (guid == AppConfig.TASKER_DEFAULT_GUID) {
+ Utils.startVServiceFromToggle(context)
+ } else {
+ mainStorage?.encode(MmkvManager.KEY_SELECTED_SERVER, guid)
+ V2RayServiceManager.startV2Ray(context)
+ }
+ } else {
+ Utils.stopVService(context)
+ }
+ } catch (e: WriterException) {
+ e.printStackTrace()
+ }
+ }
+}
diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/receiver/WidgetProvider.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/receiver/WidgetProvider.kt
new file mode 100644
index 00000000..826bf24f
--- /dev/null
+++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/receiver/WidgetProvider.kt
@@ -0,0 +1,85 @@
+package com.v2ray.ang.receiver
+
+import android.app.PendingIntent
+import android.appwidget.AppWidgetManager
+import android.appwidget.AppWidgetProvider
+import android.content.ComponentName
+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.service.V2RayServiceManager
+import com.v2ray.ang.util.Utils
+
+class WidgetProvider : AppWidgetProvider() {
+ /**
+ * 每次窗口小部件被更新都调用一次该方法
+ */
+ override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) {
+ super.onUpdate(context, appWidgetManager, appWidgetIds)
+ updateWidgetBackground(context, appWidgetManager, appWidgetIds, V2RayServiceManager.v2rayPoint.isRunning)
+ }
+
+
+ private fun updateWidgetBackground(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray, isRunning: Boolean) {
+ val remoteViews = RemoteViews(context.packageName, R.layout.widget_switch)
+ val intent = Intent(context, WidgetProvider::class.java)
+ intent.action = AppConfig.BROADCAST_ACTION_WIDGET_CLICK
+ val pendingIntent = PendingIntent.getBroadcast(
+ context,
+ R.id.layout_switch,
+ intent,
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
+ } else {
+ PendingIntent.FLAG_UPDATE_CURRENT
+ })
+ remoteViews.setOnClickPendingIntent(R.id.layout_switch, pendingIntent)
+ if (isRunning) {
+ remoteViews.setInt(
+ R.id.layout_switch,
+ "setBackgroundResource",
+ R.drawable.ic_rounded_corner_theme
+ )
+ } else {
+ remoteViews.setInt(
+ R.id.layout_switch,
+ "setBackgroundResource",
+ R.drawable.ic_rounded_corner_grey
+ )
+ }
+
+ for (appWidgetId in appWidgetIds) {
+ appWidgetManager.updateAppWidget(appWidgetId, remoteViews)
+ }
+ }
+
+ /**
+ * 接收窗口小部件发送的广播
+ */
+ override fun onReceive(context: Context, intent: Intent) {
+ super.onReceive(context, intent)
+ if (AppConfig.BROADCAST_ACTION_WIDGET_CLICK == intent.action) {
+ if (V2RayServiceManager.v2rayPoint.isRunning) {
+ Utils.stopVService(context)
+ } else {
+ Utils.startVServiceFromToggle(context)
+ }
+ } else if (AppConfig.BROADCAST_ACTION_ACTIVITY == intent.action) {
+ AppWidgetManager.getInstance(context)?.let { manager ->
+ when (intent.getIntExtra("key", 0)) {
+ AppConfig.MSG_STATE_RUNNING, AppConfig.MSG_STATE_START_SUCCESS -> {
+ updateWidgetBackground(context, manager, manager.getAppWidgetIds(ComponentName(context, WidgetProvider::class.java)),
+ true)
+ }
+ AppConfig.MSG_STATE_NOT_RUNNING, AppConfig.MSG_STATE_START_FAILURE, AppConfig.MSG_STATE_STOP_SUCCESS -> {
+ updateWidgetBackground(context, manager, manager.getAppWidgetIds(ComponentName(context, WidgetProvider::class.java)),
+ false)
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/service/QSTileService.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/service/QSTileService.kt
new file mode 100644
index 00000000..176b0fbe
--- /dev/null
+++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/service/QSTileService.kt
@@ -0,0 +1,87 @@
+package com.v2ray.ang.service
+
+import android.annotation.TargetApi
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.graphics.drawable.Icon
+import android.os.Build
+import android.service.quicksettings.Tile
+import android.service.quicksettings.TileService
+import com.v2ray.ang.AppConfig
+import com.v2ray.ang.R
+import com.v2ray.ang.util.MessageUtil
+import com.v2ray.ang.util.Utils
+import java.lang.ref.SoftReference
+
+@TargetApi(Build.VERSION_CODES.N)
+class QSTileService : TileService() {
+
+ fun setState(state: Int) {
+ if (state == Tile.STATE_INACTIVE) {
+ qsTile?.state = Tile.STATE_INACTIVE
+ qsTile?.label = getString(R.string.app_name)
+ qsTile?.icon = Icon.createWithResource(applicationContext, R.drawable.ic_stat_name)
+ } else if (state == Tile.STATE_ACTIVE) {
+ qsTile?.state = Tile.STATE_ACTIVE
+ qsTile?.label = V2RayServiceManager.currentConfig?.remarks
+ qsTile?.icon = Icon.createWithResource(applicationContext, R.drawable.ic_stat_name)
+ }
+
+ qsTile?.updateTile()
+ }
+
+ override fun onStartListening() {
+ super.onStartListening()
+ setState(Tile.STATE_INACTIVE)
+ mMsgReceive = ReceiveMessageHandler(this)
+ registerReceiver(mMsgReceive, IntentFilter(AppConfig.BROADCAST_ACTION_ACTIVITY))
+ MessageUtil.sendMsg2Service(this, AppConfig.MSG_REGISTER_CLIENT, "")
+ }
+
+ override fun onStopListening() {
+ super.onStopListening()
+
+ unregisterReceiver(mMsgReceive)
+ mMsgReceive = null
+ }
+
+ override fun onClick() {
+ super.onClick()
+ when (qsTile.state) {
+ Tile.STATE_INACTIVE -> {
+ Utils.startVServiceFromToggle(this)
+ }
+ Tile.STATE_ACTIVE -> {
+ Utils.stopVService(this)
+ }
+ }
+ }
+
+ private var mMsgReceive: BroadcastReceiver? = null
+
+ private class ReceiveMessageHandler(context: QSTileService) : BroadcastReceiver() {
+ internal var mReference: SoftReference = SoftReference(context)
+ override fun onReceive(ctx: Context?, intent: Intent?) {
+ val context = mReference.get()
+ when (intent?.getIntExtra("key", 0)) {
+ AppConfig.MSG_STATE_RUNNING -> {
+ context?.setState(Tile.STATE_ACTIVE)
+ }
+ AppConfig.MSG_STATE_NOT_RUNNING -> {
+ context?.setState(Tile.STATE_INACTIVE)
+ }
+ AppConfig.MSG_STATE_START_SUCCESS -> {
+ context?.setState(Tile.STATE_ACTIVE)
+ }
+ AppConfig.MSG_STATE_START_FAILURE -> {
+ context?.setState(Tile.STATE_INACTIVE)
+ }
+ AppConfig.MSG_STATE_STOP_SUCCESS -> {
+ context?.setState(Tile.STATE_INACTIVE)
+ }
+ }
+ }
+ }
+}
diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/service/ServiceControl.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/service/ServiceControl.kt
new file mode 100644
index 00000000..36a26b88
--- /dev/null
+++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/service/ServiceControl.kt
@@ -0,0 +1,13 @@
+package com.v2ray.ang.service
+
+import android.app.Service
+
+interface ServiceControl {
+ fun getService(): Service
+
+ fun startService()
+
+ fun stopService()
+
+ fun vpnProtect(socket: Int): Boolean
+}
diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/service/V2RayProxyOnlyService.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/service/V2RayProxyOnlyService.kt
new file mode 100644
index 00000000..3403af61
--- /dev/null
+++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/service/V2RayProxyOnlyService.kt
@@ -0,0 +1,56 @@
+package com.v2ray.ang.service
+
+import android.app.Service
+import android.content.Context
+import android.content.Intent
+import android.os.Build
+import android.os.IBinder
+import androidx.annotation.RequiresApi
+import com.v2ray.ang.util.MyContextWrapper
+import com.v2ray.ang.util.Utils
+import java.lang.ref.SoftReference
+
+class V2RayProxyOnlyService : Service(), ServiceControl {
+ override fun onCreate() {
+ super.onCreate()
+ V2RayServiceManager.serviceControl = SoftReference(this)
+ }
+
+ override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
+ V2RayServiceManager.startV2rayPoint()
+ return START_STICKY
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+ V2RayServiceManager.stopV2rayPoint()
+ }
+
+ override fun getService(): Service {
+ return this
+ }
+
+ override fun startService() {
+ // do nothing
+ }
+
+ override fun stopService() {
+ stopSelf()
+ }
+
+ override fun vpnProtect(socket: Int): Boolean {
+ return true
+ }
+
+ override fun onBind(intent: Intent?): IBinder? {
+ return null
+ }
+
+ @RequiresApi(Build.VERSION_CODES.N)
+ override fun attachBaseContext(newBase: Context?) {
+ val context = newBase?.let {
+ MyContextWrapper.wrap(newBase, Utils.getLocale(newBase))
+ }
+ super.attachBaseContext(context)
+ }
+}
diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/service/V2RayServiceManager.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/service/V2RayServiceManager.kt
new file mode 100644
index 00000000..15f4a1e8
--- /dev/null
+++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/service/V2RayServiceManager.kt
@@ -0,0 +1,399 @@
+package com.v2ray.ang.service
+
+import android.app.*
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.graphics.Color
+import android.os.Build
+import android.util.Log
+import androidx.annotation.RequiresApi
+import androidx.core.app.NotificationCompat
+import com.tencent.mmkv.MMKV
+import com.v2ray.ang.AppConfig
+import com.v2ray.ang.AppConfig.ANG_PACKAGE
+import com.v2ray.ang.AppConfig.TAG_DIRECT
+import com.v2ray.ang.R
+import com.v2ray.ang.dto.ServerConfig
+import com.v2ray.ang.extension.toSpeedString
+import com.v2ray.ang.extension.toast
+import com.v2ray.ang.ui.MainActivity
+import com.v2ray.ang.util.MessageUtil
+import com.v2ray.ang.util.MmkvManager
+import com.v2ray.ang.util.Utils
+import com.v2ray.ang.util.V2rayConfigUtil
+import go.Seq
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.GlobalScope
+import kotlinx.coroutines.launch
+import libv2ray.Libv2ray
+import libv2ray.V2RayPoint
+import libv2ray.V2RayVPNServiceSupportsSet
+import rx.Observable
+import rx.Subscription
+import java.lang.ref.SoftReference
+import kotlin.math.min
+
+object V2RayServiceManager {
+ private const val NOTIFICATION_ID = 1
+ private const val NOTIFICATION_PENDING_INTENT_CONTENT = 0
+ private const val NOTIFICATION_PENDING_INTENT_STOP_V2RAY = 1
+ private const val NOTIFICATION_ICON_THRESHOLD = 3000
+
+ val v2rayPoint: V2RayPoint = Libv2ray.newV2RayPoint(V2RayCallback(), Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1)
+ private val mMsgReceive = ReceiveMessageHandler()
+ private val mainStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_MAIN, MMKV.MULTI_PROCESS_MODE) }
+ private val settingsStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_SETTING, MMKV.MULTI_PROCESS_MODE) }
+
+ var serviceControl: SoftReference? = null
+ set(value) {
+ field = value
+ Seq.setContext(value?.get()?.getService()?.applicationContext)
+ Libv2ray.initV2Env(Utils.userAssetPath(value?.get()?.getService()))
+ }
+ var currentConfig: ServerConfig? = null
+
+ private var lastQueryTime = 0L
+ private var mBuilder: NotificationCompat.Builder? = null
+ private var mSubscription: Subscription? = null
+ private var mNotificationManager: NotificationManager? = null
+
+ fun startV2Ray(context: Context) {
+ if (settingsStorage?.decodeBool(AppConfig.PREF_PROXY_SHARING) == true) {
+ context.toast(R.string.toast_warning_pref_proxysharing_short)
+ } else {
+ context.toast(R.string.toast_services_start)
+ }
+ val intent = if (settingsStorage?.decodeString(AppConfig.PREF_MODE) ?: "VPN" == "VPN") {
+ Intent(context.applicationContext, V2RayVpnService::class.java)
+ } else {
+ Intent(context.applicationContext, V2RayProxyOnlyService::class.java)
+ }
+ if (Build.VERSION.SDK_INT > Build.VERSION_CODES.N_MR1) {
+ context.startForegroundService(intent)
+ } else {
+ context.startService(intent)
+ }
+ }
+
+ private class V2RayCallback : V2RayVPNServiceSupportsSet {
+ override fun shutdown(): Long {
+ val serviceControl = serviceControl?.get() ?: return -1
+ // called by go
+ return try {
+ serviceControl.stopService()
+ 0
+ } catch (e: Exception) {
+ Log.d(ANG_PACKAGE, e.toString())
+ -1
+ }
+ }
+
+ override fun prepare(): Long {
+ return 0
+ }
+
+ override fun protect(l: Long): Boolean {
+ val serviceControl = serviceControl?.get() ?: return true
+ return serviceControl.vpnProtect(l.toInt())
+ }
+
+ override fun onEmitStatus(l: Long, s: String?): Long {
+ //Logger.d(s)
+ return 0
+ }
+
+ override fun setup(s: String): Long {
+ val serviceControl = serviceControl?.get() ?: return -1
+ //Logger.d(s)
+ return try {
+ serviceControl.startService()
+ lastQueryTime = System.currentTimeMillis()
+ startSpeedNotification()
+ 0
+ } catch (e: Exception) {
+ Log.d(ANG_PACKAGE, e.toString())
+ -1
+ }
+ }
+ }
+
+ fun startV2rayPoint() {
+ val service = serviceControl?.get()?.getService() ?: return
+ val guid = mainStorage?.decodeString(MmkvManager.KEY_SELECTED_SERVER) ?: return
+ val config = MmkvManager.decodeServerConfig(guid) ?: return
+ if (!v2rayPoint.isRunning) {
+ val result = V2rayConfigUtil.getV2rayConfig(service, guid)
+ if (!result.status)
+ return
+
+ try {
+ val mFilter = IntentFilter(AppConfig.BROADCAST_ACTION_SERVICE)
+ mFilter.addAction(Intent.ACTION_SCREEN_ON)
+ mFilter.addAction(Intent.ACTION_SCREEN_OFF)
+ mFilter.addAction(Intent.ACTION_USER_PRESENT)
+ service.registerReceiver(mMsgReceive, mFilter)
+ } catch (e: Exception) {
+ Log.d(ANG_PACKAGE, e.toString())
+ }
+
+ v2rayPoint.configureFileContent = result.content
+ v2rayPoint.domainName = config.getV2rayPointDomainAndPort()
+ currentConfig = config
+
+ try {
+ v2rayPoint.runLoop(settingsStorage?.decodeBool(AppConfig.PREF_PREFER_IPV6) ?: false)
+ } catch (e: Exception) {
+ Log.d(ANG_PACKAGE, e.toString())
+ }
+
+ if (v2rayPoint.isRunning) {
+ MessageUtil.sendMsg2UI(service, AppConfig.MSG_STATE_START_SUCCESS, "")
+ showNotification()
+ } else {
+ MessageUtil.sendMsg2UI(service, AppConfig.MSG_STATE_START_FAILURE, "")
+ cancelNotification()
+ }
+ }
+ }
+
+ fun stopV2rayPoint() {
+ val service = serviceControl?.get()?.getService() ?: return
+
+ if (v2rayPoint.isRunning) {
+ GlobalScope.launch(Dispatchers.Default) {
+ try {
+ v2rayPoint.stopLoop()
+ } catch (e: Exception) {
+ Log.d(ANG_PACKAGE, e.toString())
+ }
+ }
+ }
+
+ MessageUtil.sendMsg2UI(service, AppConfig.MSG_STATE_STOP_SUCCESS, "")
+ cancelNotification()
+
+ try {
+ service.unregisterReceiver(mMsgReceive)
+ } catch (e: Exception) {
+ Log.d(ANG_PACKAGE, e.toString())
+ }
+ }
+
+ private class ReceiveMessageHandler : BroadcastReceiver() {
+ override fun onReceive(ctx: Context?, intent: Intent?) {
+ val serviceControl = serviceControl?.get() ?: return
+ when (intent?.getIntExtra("key", 0)) {
+ AppConfig.MSG_REGISTER_CLIENT -> {
+ //Logger.e("ReceiveMessageHandler", intent?.getIntExtra("key", 0).toString())
+ if (v2rayPoint.isRunning) {
+ MessageUtil.sendMsg2UI(serviceControl.getService(), AppConfig.MSG_STATE_RUNNING, "")
+ } else {
+ MessageUtil.sendMsg2UI(serviceControl.getService(), AppConfig.MSG_STATE_NOT_RUNNING, "")
+ }
+ }
+ AppConfig.MSG_UNREGISTER_CLIENT -> {
+ // nothing to do
+ }
+ AppConfig.MSG_STATE_START -> {
+ // nothing to do
+ }
+ AppConfig.MSG_STATE_STOP -> {
+ serviceControl.stopService()
+ }
+ AppConfig.MSG_STATE_RESTART -> {
+ startV2rayPoint()
+ }
+ AppConfig.MSG_MEASURE_DELAY -> {
+ measureV2rayDelay()
+ }
+ }
+
+ when (intent?.action) {
+ Intent.ACTION_SCREEN_OFF -> {
+ Log.d(ANG_PACKAGE, "SCREEN_OFF, stop querying stats")
+ stopSpeedNotification()
+ }
+ Intent.ACTION_SCREEN_ON -> {
+ Log.d(ANG_PACKAGE, "SCREEN_ON, start querying stats")
+ startSpeedNotification()
+ }
+ }
+ }
+ }
+
+ private fun measureV2rayDelay() {
+ GlobalScope.launch(Dispatchers.IO) {
+ val service = serviceControl?.get()?.getService() ?: return@launch
+ var time = -1L
+ var errstr = ""
+ if (v2rayPoint.isRunning) {
+ try {
+ time = v2rayPoint.measureDelay()
+ } catch (e: Exception) {
+ Log.d(ANG_PACKAGE, "measureV2rayDelay: $e")
+ errstr = e.message?.substringAfter("\":") ?: "empty message"
+ }
+ }
+ val result = if (time == -1L) {
+ service.getString(R.string.connection_test_error, errstr)
+ } else {
+ service.getString(R.string.connection_test_available, time)
+ }
+
+ MessageUtil.sendMsg2UI(service, AppConfig.MSG_MEASURE_DELAY_SUCCESS, result)
+ }
+ }
+
+ private fun showNotification() {
+ val service = serviceControl?.get()?.getService() ?: return
+ val startMainIntent = Intent(service, MainActivity::class.java)
+ val contentPendingIntent = PendingIntent.getActivity(service,
+ NOTIFICATION_PENDING_INTENT_CONTENT, startMainIntent,
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
+ } else {
+ PendingIntent.FLAG_UPDATE_CURRENT
+ })
+
+ val stopV2RayIntent = Intent(AppConfig.BROADCAST_ACTION_SERVICE)
+ stopV2RayIntent.`package` = ANG_PACKAGE
+ stopV2RayIntent.putExtra("key", AppConfig.MSG_STATE_STOP)
+
+ val stopV2RayPendingIntent = PendingIntent.getBroadcast(service,
+ NOTIFICATION_PENDING_INTENT_STOP_V2RAY, stopV2RayIntent,
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
+ } else {
+ PendingIntent.FLAG_UPDATE_CURRENT
+ })
+
+ val channelId =
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ createNotificationChannel()
+ } else {
+ // If earlier version channel ID is not used
+ // https://developer.android.com/reference/android/support/v4/app/NotificationCompat.Builder.html#NotificationCompat.Builder(android.content.Context)
+ ""
+ }
+
+ mBuilder = NotificationCompat.Builder(service, channelId)
+ .setSmallIcon(R.drawable.ic_stat_name)
+ .setContentTitle(currentConfig?.remarks)
+ .setPriority(NotificationCompat.PRIORITY_MIN)
+ .setOngoing(true)
+ .setShowWhen(false)
+ .setOnlyAlertOnce(true)
+ .setContentIntent(contentPendingIntent)
+ .addAction(R.drawable.ic_close_grey_800_24dp,
+ service.getString(R.string.notification_action_stop_v2ray),
+ stopV2RayPendingIntent)
+ //.build()
+
+ //mBuilder?.setDefaults(NotificationCompat.FLAG_ONLY_ALERT_ONCE) //取消震动,铃声其他都不好使
+
+ service.startForeground(NOTIFICATION_ID, mBuilder?.build())
+ }
+
+ @RequiresApi(Build.VERSION_CODES.O)
+ private fun createNotificationChannel(): String {
+ val channelId = "RAY_NG_M_CH_ID"
+ val channelName = "V2rayNG Background Service"
+ val chan = NotificationChannel(channelId,
+ channelName, NotificationManager.IMPORTANCE_HIGH)
+ chan.lightColor = Color.DKGRAY
+ chan.importance = NotificationManager.IMPORTANCE_NONE
+ chan.lockscreenVisibility = Notification.VISIBILITY_PRIVATE
+ getNotificationManager()?.createNotificationChannel(chan)
+ return channelId
+ }
+
+ fun cancelNotification() {
+ val service = serviceControl?.get()?.getService() ?: return
+ service.stopForeground(true)
+ mBuilder = null
+ mSubscription?.unsubscribe()
+ mSubscription = null
+ }
+
+ private fun updateNotification(contentText: String?, proxyTraffic: Long, directTraffic: Long) {
+ if (mBuilder != null) {
+ if (proxyTraffic < NOTIFICATION_ICON_THRESHOLD && directTraffic < NOTIFICATION_ICON_THRESHOLD) {
+ mBuilder?.setSmallIcon(R.drawable.ic_stat_name)
+ } else if (proxyTraffic > directTraffic) {
+ mBuilder?.setSmallIcon(R.drawable.ic_stat_proxy)
+ } else {
+ mBuilder?.setSmallIcon(R.drawable.ic_stat_direct)
+ }
+ mBuilder?.setStyle(NotificationCompat.BigTextStyle().bigText(contentText))
+ mBuilder?.setContentText(contentText) // Emui4.1 need content text even if style is set as BigTextStyle
+ getNotificationManager()?.notify(NOTIFICATION_ID, mBuilder?.build())
+ }
+ }
+
+ private fun getNotificationManager(): NotificationManager? {
+ if (mNotificationManager == null) {
+ val service = serviceControl?.get()?.getService() ?: return null
+ mNotificationManager = service.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
+ }
+ return mNotificationManager
+ }
+
+ private fun startSpeedNotification() {
+ if (mSubscription == null &&
+ v2rayPoint.isRunning &&
+ settingsStorage?.decodeBool(AppConfig.PREF_SPEED_ENABLED) == true) {
+ var lastZeroSpeed = false
+ val outboundTags = currentConfig?.getAllOutboundTags()
+ outboundTags?.remove(TAG_DIRECT)
+
+ mSubscription = Observable.interval(3, java.util.concurrent.TimeUnit.SECONDS)
+ .subscribe {
+ val queryTime = System.currentTimeMillis()
+ val sinceLastQueryInSeconds = (queryTime - lastQueryTime) / 1000.0
+ var proxyTotal = 0L
+ val text = StringBuilder()
+ outboundTags?.forEach {
+ val up = v2rayPoint.queryStats(it, "uplink")
+ val down = v2rayPoint.queryStats(it, "downlink")
+ if (up + down > 0) {
+ appendSpeedString(text, it, up / sinceLastQueryInSeconds, down / sinceLastQueryInSeconds)
+ proxyTotal += up + down
+ }
+ }
+ val directUplink = v2rayPoint.queryStats(TAG_DIRECT, "uplink")
+ val directDownlink = v2rayPoint.queryStats(TAG_DIRECT, "downlink")
+ val zeroSpeed = (proxyTotal == 0L && directUplink == 0L && directDownlink == 0L)
+ if (!zeroSpeed || !lastZeroSpeed) {
+ if (proxyTotal == 0L) {
+ appendSpeedString(text, outboundTags?.firstOrNull(), 0.0, 0.0)
+ }
+ appendSpeedString(text, TAG_DIRECT, directUplink / sinceLastQueryInSeconds,
+ directDownlink / sinceLastQueryInSeconds)
+ updateNotification(text.toString(), proxyTotal, directDownlink + directUplink)
+ }
+ lastZeroSpeed = zeroSpeed
+ lastQueryTime = queryTime
+ }
+ }
+ }
+
+ private fun appendSpeedString(text: StringBuilder, name: String?, up: Double, down: Double) {
+ var n = name ?: "no tag"
+ n = n.substring(0, min(n.length, 6))
+ text.append(n)
+ for (i in n.length..6 step 2) {
+ text.append("\t")
+ }
+ text.append("• ${up.toLong().toSpeedString()}↑ ${down.toLong().toSpeedString()}↓\n")
+ }
+
+ private fun stopSpeedNotification() {
+ if (mSubscription != null) {
+ mSubscription?.unsubscribe() //stop queryStats
+ mSubscription = null
+ updateNotification(currentConfig?.remarks, 0, 0)
+ }
+ }
+}
diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/service/V2RayTestService.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/service/V2RayTestService.kt
new file mode 100644
index 00000000..362b7196
--- /dev/null
+++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/service/V2RayTestService.kt
@@ -0,0 +1,45 @@
+package com.v2ray.ang.service
+
+import android.app.Service
+import android.content.Intent
+import android.os.IBinder
+import com.v2ray.ang.AppConfig.MSG_MEASURE_CONFIG
+import com.v2ray.ang.AppConfig.MSG_MEASURE_CONFIG_CANCEL
+import com.v2ray.ang.AppConfig.MSG_MEASURE_CONFIG_SUCCESS
+import com.v2ray.ang.util.MessageUtil
+import com.v2ray.ang.util.SpeedtestUtil
+import com.v2ray.ang.util.Utils
+import go.Seq
+import kotlinx.coroutines.*
+import libv2ray.Libv2ray
+import java.util.concurrent.Executors
+
+class V2RayTestService : Service() {
+ private val realTestScope by lazy { CoroutineScope(Executors.newFixedThreadPool(10).asCoroutineDispatcher()) }
+
+ override fun onCreate() {
+ super.onCreate()
+ Seq.setContext(this)
+ Libv2ray.initV2Env(Utils.userAssetPath(this))
+ }
+
+ override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
+ when (intent?.getIntExtra("key", 0)) {
+ MSG_MEASURE_CONFIG -> {
+ val contentPair = intent.getSerializableExtra("content") as Pair
+ realTestScope.launch {
+ val result = SpeedtestUtil.realPing(contentPair.second)
+ MessageUtil.sendMsg2UI(this@V2RayTestService, MSG_MEASURE_CONFIG_SUCCESS, Pair(contentPair.first, result))
+ }
+ }
+ MSG_MEASURE_CONFIG_CANCEL -> {
+ realTestScope.coroutineContext[Job]?.cancelChildren()
+ }
+ }
+ return super.onStartCommand(intent, flags, startId)
+ }
+
+ override fun onBind(intent: Intent?): IBinder? {
+ return null
+ }
+}
diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/service/V2RayVpnService.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/service/V2RayVpnService.kt
new file mode 100644
index 00000000..af17b466
--- /dev/null
+++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/service/V2RayVpnService.kt
@@ -0,0 +1,333 @@
+package com.v2ray.ang.service
+
+import android.app.Service
+import android.content.Context
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.net.*
+import android.os.Build
+import android.os.ParcelFileDescriptor
+import android.os.StrictMode
+import android.util.Log
+import androidx.annotation.RequiresApi
+import com.tencent.mmkv.MMKV
+import com.v2ray.ang.AppConfig
+import com.v2ray.ang.R
+import com.v2ray.ang.dto.ERoutingMode
+import com.v2ray.ang.util.MmkvManager
+import com.v2ray.ang.util.MyContextWrapper
+import com.v2ray.ang.util.Utils
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.GlobalScope
+import kotlinx.coroutines.launch
+import java.io.File
+import java.lang.ref.SoftReference
+
+class V2RayVpnService : VpnService(), ServiceControl {
+ companion object {
+ private const val VPN_MTU = 1500
+ private const val PRIVATE_VLAN4_CLIENT = "26.26.26.1"
+ private const val PRIVATE_VLAN4_ROUTER = "26.26.26.2"
+ private const val PRIVATE_VLAN6_CLIENT = "da26:2626::1"
+ private const val PRIVATE_VLAN6_ROUTER = "da26:2626::2"
+ private const val TUN2SOCKS = "libtun2socks.so"
+ }
+
+ private val settingsStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_SETTING, MMKV.MULTI_PROCESS_MODE) }
+
+ private lateinit var mInterface: ParcelFileDescriptor
+ private var isRunning = false
+
+ //val fd: Int get() = mInterface.fd
+ private lateinit var process: Process
+
+ /**destroy
+ * Unfortunately registerDefaultNetworkCallback is going to return our VPN interface: https://android.googlesource.com/platform/frameworks/base/+/dda156ab0c5d66ad82bdcf76cda07cbc0a9c8a2e
+ *
+ * This makes doing a requestNetwork with REQUEST necessary so that we don't get ALL possible networks that
+ * satisfies default network capabilities but only THE default network. Unfortunately we need to have
+ * android.permission.CHANGE_NETWORK_STATE to be able to call requestNetwork.
+ *
+ * Source: https://android.googlesource.com/platform/frameworks/base/+/2df4c7d/services/core/java/com/android/server/ConnectivityService.java#887
+ */
+ @delegate:RequiresApi(Build.VERSION_CODES.P)
+ private val defaultNetworkRequest by lazy {
+ NetworkRequest.Builder()
+ .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
+ .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED)
+ .build()
+ }
+
+ private val connectivity by lazy { getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager }
+
+ @delegate:RequiresApi(Build.VERSION_CODES.P)
+ private val defaultNetworkCallback by lazy {
+ object : ConnectivityManager.NetworkCallback() {
+ override fun onAvailable(network: Network) {
+ setUnderlyingNetworks(arrayOf(network))
+ }
+
+ override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) {
+ // it's a good idea to refresh capabilities
+ setUnderlyingNetworks(arrayOf(network))
+ }
+
+ override fun onLost(network: Network) {
+ setUnderlyingNetworks(null)
+ }
+ }
+ }
+
+ override fun onCreate() {
+ super.onCreate()
+
+ val policy = StrictMode.ThreadPolicy.Builder().permitAll().build()
+ StrictMode.setThreadPolicy(policy)
+ V2RayServiceManager.serviceControl = SoftReference(this)
+ }
+
+ override fun onRevoke() {
+ stopV2Ray()
+ }
+
+// override fun onLowMemory() {
+// stopV2Ray()
+// super.onLowMemory()
+// }
+
+ override fun onDestroy() {
+ super.onDestroy()
+ V2RayServiceManager.cancelNotification()
+ }
+
+ private fun setup() {
+ val prepare = prepare(this)
+ if (prepare != null) {
+ return
+ }
+
+ // If the old interface has exactly the same parameters, use it!
+ // Configure a builder while parsing the parameters.
+ val builder = Builder()
+ //val enableLocalDns = defaultDPreference.getPrefBoolean(AppConfig.PREF_LOCAL_DNS_ENABLED, false)
+
+ val routingMode = settingsStorage?.decodeString(AppConfig.PREF_ROUTING_MODE) ?: ERoutingMode.GLOBAL_PROXY.value
+
+ builder.setMtu(VPN_MTU)
+ builder.addAddress(PRIVATE_VLAN4_CLIENT, 30)
+ //builder.addDnsServer(PRIVATE_VLAN4_ROUTER)
+ if (routingMode == ERoutingMode.BYPASS_LAN.value || routingMode == ERoutingMode.BYPASS_LAN_MAINLAND.value) {
+ resources.getStringArray(R.array.bypass_private_ip_address).forEach {
+ val addr = it.split('/')
+ builder.addRoute(addr[0], addr[1].toInt())
+ }
+ } else {
+ builder.addRoute("0.0.0.0", 0)
+ }
+
+ if (settingsStorage?.decodeBool(AppConfig.PREF_PREFER_IPV6) == true) {
+ builder.addAddress(PRIVATE_VLAN6_CLIENT, 126)
+ if (routingMode == ERoutingMode.BYPASS_LAN.value || routingMode == ERoutingMode.BYPASS_LAN_MAINLAND.value) {
+ builder.addRoute("2000::", 3) //currently only 1/8 of total ipV6 is in use
+ } else {
+ builder.addRoute("::", 0)
+ }
+ }
+
+ if (settingsStorage?.decodeBool(AppConfig.PREF_LOCAL_DNS_ENABLED) == true) {
+ builder.addDnsServer(PRIVATE_VLAN4_ROUTER)
+ } else {
+ Utils.getVpnDnsServers()
+ .forEach {
+ if (Utils.isPureIpAddress(it)) {
+ builder.addDnsServer(it)
+ }
+ }
+ }
+
+ builder.setSession(V2RayServiceManager.currentConfig?.remarks.orEmpty())
+
+ if (settingsStorage?.decodeBool(AppConfig.PREF_PER_APP_PROXY) == true) {
+ val apps = settingsStorage?.decodeStringSet(AppConfig.PREF_PER_APP_PROXY_SET)
+ val bypassApps = settingsStorage?.decodeBool(AppConfig.PREF_BYPASS_APPS) ?: false
+ apps?.forEach {
+ try {
+ if (bypassApps)
+ builder.addDisallowedApplication(it)
+ else
+ builder.addAllowedApplication(it)
+ } catch (e: PackageManager.NameNotFoundException) {
+ //Logger.d(e)
+ }
+ }
+ }
+
+ // Close the old interface since the parameters have been changed.
+ try {
+ mInterface.close()
+ } catch (ignored: Exception) {
+ // ignored
+ }
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
+ try {
+ connectivity.requestNetwork(defaultNetworkRequest, defaultNetworkCallback)
+ } catch (e: Exception) {
+ e.printStackTrace()
+ }
+ }
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ builder.setMetered(false)
+ }
+
+ // Create a new interface using the builder and save the parameters.
+ try {
+ mInterface = builder.establish()!!
+ isRunning = true
+ runTun2socks()
+ } catch (e: Exception) {
+ // non-nullable lateinit var
+ e.printStackTrace()
+ stopV2Ray()
+ }
+ }
+
+ private fun runTun2socks() {
+ val socksPort = Utils.parseInt(settingsStorage?.decodeString(AppConfig.PREF_SOCKS_PORT), AppConfig.PORT_SOCKS.toInt())
+ val cmd = arrayListOf(File(applicationContext.applicationInfo.nativeLibraryDir, TUN2SOCKS).absolutePath,
+ "--netif-ipaddr", PRIVATE_VLAN4_ROUTER,
+ "--netif-netmask", "255.255.255.252",
+ "--socks-server-addr", "127.0.0.1:${socksPort}",
+ "--tunmtu", VPN_MTU.toString(),
+ "--sock-path", "sock_path",//File(applicationContext.filesDir, "sock_path").absolutePath,
+ "--enable-udprelay",
+ "--loglevel", "notice")
+
+ if (settingsStorage?.decodeBool(AppConfig.PREF_PREFER_IPV6) == true) {
+ cmd.add("--netif-ip6addr")
+ cmd.add(PRIVATE_VLAN6_ROUTER)
+ }
+ if (settingsStorage?.decodeBool(AppConfig.PREF_LOCAL_DNS_ENABLED) == true) {
+ val localDnsPort = Utils.parseInt(settingsStorage?.decodeString(AppConfig.PREF_LOCAL_DNS_PORT), AppConfig.PORT_LOCAL_DNS.toInt())
+ cmd.add("--dnsgw")
+ cmd.add("127.0.0.1:${localDnsPort}")
+ }
+ Log.d(packageName, cmd.toString())
+
+ try {
+ val proBuilder = ProcessBuilder(cmd)
+ proBuilder.redirectErrorStream(true)
+ process = proBuilder
+ .directory(applicationContext.filesDir)
+ .start()
+ Thread(Runnable {
+ Log.d(packageName,"$TUN2SOCKS check")
+ process.waitFor()
+ Log.d(packageName,"$TUN2SOCKS exited")
+ if (isRunning) {
+ Log.d(packageName,"$TUN2SOCKS restart")
+ runTun2socks()
+ }
+ }).start()
+ Log.d(packageName, process.toString())
+
+ sendFd()
+ } catch (e: Exception) {
+ Log.d(packageName, e.toString())
+ }
+ }
+
+ private fun sendFd() {
+ val fd = mInterface.fileDescriptor
+ val path = File(applicationContext.filesDir, "sock_path").absolutePath
+ Log.d(packageName, path)
+
+ GlobalScope.launch(Dispatchers.IO) {
+ var tries = 0
+ while (true) try {
+ Thread.sleep(50L shl tries)
+ Log.d(packageName, "sendFd tries: $tries")
+ LocalSocket().use { localSocket ->
+ localSocket.connect(LocalSocketAddress(path, LocalSocketAddress.Namespace.FILESYSTEM))
+ localSocket.setFileDescriptorsForSend(arrayOf(fd))
+ localSocket.outputStream.write(42)
+ }
+ break
+ } catch (e: Exception) {
+ Log.d(packageName, e.toString())
+ if (tries > 5) break
+ tries += 1
+ }
+ }
+ }
+
+ override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
+ V2RayServiceManager.startV2rayPoint()
+ return START_STICKY
+ //return super.onStartCommand(intent, flags, startId)
+ }
+
+ private fun stopV2Ray(isForced: Boolean = true) {
+// val configName = defaultDPreference.getPrefString(PREF_CURR_CONFIG_GUID, "")
+// val emptyInfo = VpnNetworkInfo()
+// val info = loadVpnNetworkInfo(configName, emptyInfo)!! + (lastNetworkInfo ?: emptyInfo)
+// saveVpnNetworkInfo(configName, info)
+ isRunning = false;
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
+ try {
+ connectivity.unregisterNetworkCallback(defaultNetworkCallback)
+ } catch (ignored: Exception) {
+ // ignored
+ }
+ }
+
+ try {
+ Log.d(packageName, "tun2socks destroy")
+ process.destroy()
+ } catch (e: Exception) {
+ Log.d(packageName, e.toString())
+ }
+
+ V2RayServiceManager.stopV2rayPoint()
+
+ if (isForced) {
+ //stopSelf has to be called ahead of mInterface.close(). otherwise v2ray core cannot be stooped
+ //It's strage but true.
+ //This can be verified by putting stopself() behind and call stopLoop and startLoop
+ //in a row for several times. You will find that later created v2ray core report port in use
+ //which means the first v2ray core somehow failed to stop and release the port.
+ stopSelf()
+
+ try {
+ mInterface.close()
+ } catch (ignored: Exception) {
+ // ignored
+ }
+ }
+ }
+
+ override fun getService(): Service {
+ return this
+ }
+
+ override fun startService() {
+ setup()
+ }
+
+ override fun stopService() {
+ stopV2Ray(true)
+ }
+
+ override fun vpnProtect(socket: Int): Boolean {
+ return protect(socket)
+ }
+
+ @RequiresApi(Build.VERSION_CODES.N)
+ override fun attachBaseContext(newBase: Context?) {
+ val context = newBase?.let {
+ MyContextWrapper.wrap(newBase, Utils.getLocale(newBase))
+ }
+ super.attachBaseContext(context)
+ }
+}
diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/BaseActivity.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/BaseActivity.kt
new file mode 100644
index 00000000..66d8feb6
--- /dev/null
+++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/BaseActivity.kt
@@ -0,0 +1,53 @@
+package com.v2ray.ang.ui
+
+import android.content.Context
+import android.os.Build
+import android.os.Bundle
+import androidx.appcompat.app.AppCompatActivity
+import android.view.MenuItem
+import androidx.annotation.RequiresApi
+import com.v2ray.ang.util.MyContextWrapper
+import com.v2ray.ang.R
+import com.v2ray.ang.util.Utils
+
+abstract class BaseActivity : AppCompatActivity() {
+ override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
+ android.R.id.home -> {
+ onBackPressed()
+ true
+ }
+ else -> super.onOptionsItemSelected(item)
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ checkDarkMode()
+ }
+
+ private fun checkDarkMode() {
+ if (Utils.getDarkModeStatus(this)) {
+ if (this.javaClass.simpleName == "MainActivity") {
+ setTheme(R.style.AppThemeDark_NoActionBar)
+ } else {
+ setTheme(R.style.AppThemeDark)
+ }
+ } else {
+ if (this.javaClass.simpleName == "MainActivity") {
+ setTheme(R.style.AppThemeLight_NoActionBar)
+ } else {
+ setTheme(R.style.AppThemeLight)
+ }
+ }
+ }
+
+ @RequiresApi(Build.VERSION_CODES.N)
+ override fun attachBaseContext(newBase: Context?) {
+ val context = newBase?.let {
+ MyContextWrapper.wrap(newBase, Utils.getLocale(newBase))
+ }
+ super.attachBaseContext(context)
+ }
+
+
+
+}
\ No newline at end of file
diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/FragmentAdapter.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/FragmentAdapter.kt
new file mode 100644
index 00000000..d9d12992
--- /dev/null
+++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/FragmentAdapter.kt
@@ -0,0 +1,17 @@
+package com.v2ray.ang.ui
+
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.FragmentActivity
+import androidx.viewpager2.adapter.FragmentStateAdapter
+
+class FragmentAdapter(fragmentActivity: FragmentActivity, private val mFragments: List) :
+ FragmentStateAdapter(fragmentActivity) {
+
+ override fun createFragment(position: Int): Fragment {
+ return mFragments[position]
+ }
+
+ override fun getItemCount(): Int {
+ return mFragments.size
+ }
+}
diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/LogcatActivity.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/LogcatActivity.kt
new file mode 100644
index 00000000..08d76490
--- /dev/null
+++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/LogcatActivity.kt
@@ -0,0 +1,92 @@
+package com.v2ray.ang.ui
+
+import android.os.Handler
+import android.os.Looper
+import android.os.Bundle
+import android.text.method.ScrollingMovementMethod
+import android.view.Menu
+import android.view.MenuItem
+import android.view.View
+import androidx.lifecycle.lifecycleScope
+import com.v2ray.ang.AppConfig.ANG_PACKAGE
+import com.v2ray.ang.R
+import com.v2ray.ang.databinding.ActivityLogcatBinding
+import com.v2ray.ang.extension.toast
+import com.v2ray.ang.util.Utils
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.GlobalScope
+import kotlinx.coroutines.launch
+
+import java.io.IOException
+import java.util.LinkedHashSet
+
+class LogcatActivity : BaseActivity() {
+ private lateinit var binding: ActivityLogcatBinding
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ binding = ActivityLogcatBinding.inflate(layoutInflater)
+ val view = binding.root
+ setContentView(view)
+
+ title = getString(R.string.title_logcat)
+
+ supportActionBar?.setDisplayHomeAsUpEnabled(true)
+ logcat(false)
+ }
+
+ private fun logcat(shouldFlushLog: Boolean) {
+
+ try {
+ binding.pbWaiting.visibility = View.VISIBLE
+
+ lifecycleScope.launch(Dispatchers.Default) {
+ if (shouldFlushLog) {
+ val lst = LinkedHashSet()
+ lst.add("logcat")
+ lst.add("-c")
+ val process = Runtime.getRuntime().exec(lst.toTypedArray())
+ process.waitFor()
+ }
+ val lst = LinkedHashSet()
+ lst.add("logcat")
+ lst.add("-d")
+ lst.add("-v")
+ lst.add("time")
+ lst.add("-s")
+ lst.add("GoLog,tun2socks,${ANG_PACKAGE},AndroidRuntime,System.err")
+ val process = Runtime.getRuntime().exec(lst.toTypedArray())
+// val bufferedReader = BufferedReader(
+// InputStreamReader(process.inputStream))
+// val allText = bufferedReader.use(BufferedReader::readText)
+ val allText = process.inputStream.bufferedReader().use { it.readText() }
+ launch(Dispatchers.Main) {
+ binding.tvLogcat.text = allText
+ binding.tvLogcat.movementMethod = ScrollingMovementMethod()
+ binding.pbWaiting.visibility = View.GONE
+ Handler(Looper.getMainLooper()).post { binding.svLogcat.fullScroll(View.FOCUS_DOWN) }
+ }
+ }
+ } catch (e: IOException) {
+ e.printStackTrace()
+ }
+ }
+
+ override fun onCreateOptionsMenu(menu: Menu): Boolean {
+ menuInflater.inflate(R.menu.menu_logcat, menu)
+ return super.onCreateOptionsMenu(menu)
+ }
+
+ override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
+ R.id.copy_all -> {
+ Utils.setClipboard(this, binding.tvLogcat.text.toString())
+ toast(R.string.toast_success)
+ true
+ }
+ R.id.clear_all -> {
+ logcat(true)
+ true
+ }
+ else -> super.onOptionsItemSelected(item)
+ }
+}
diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/MainActivity.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/MainActivity.kt
new file mode 100644
index 00000000..d7caf53e
--- /dev/null
+++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/MainActivity.kt
@@ -0,0 +1,658 @@
+package com.v2ray.ang.ui
+
+import android.Manifest
+import android.content.*
+import android.net.Uri
+import android.net.VpnService
+import androidx.recyclerview.widget.LinearLayoutManager
+import android.view.Menu
+import android.view.MenuItem
+import com.tbruyelle.rxpermissions.RxPermissions
+import com.v2ray.ang.R
+import android.os.Bundle
+import android.text.TextUtils
+import android.view.KeyEvent
+import com.v2ray.ang.AppConfig
+import android.content.res.ColorStateList
+import com.google.android.material.navigation.NavigationView
+import androidx.core.content.ContextCompat
+import androidx.core.view.GravityCompat
+import androidx.appcompat.app.ActionBarDrawerToggle
+import androidx.recyclerview.widget.ItemTouchHelper
+import android.util.Log
+import android.widget.Toast
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.activity.viewModels
+import androidx.appcompat.app.AlertDialog
+import androidx.lifecycle.lifecycleScope
+import com.tencent.mmkv.MMKV
+import com.v2ray.ang.AppConfig.ANG_PACKAGE
+import com.v2ray.ang.BuildConfig
+import com.v2ray.ang.databinding.ActivityMainBinding
+import com.v2ray.ang.dto.EConfigType
+import com.v2ray.ang.extension.toast
+import rx.Observable
+import rx.android.schedulers.AndroidSchedulers
+import java.util.concurrent.TimeUnit
+import com.v2ray.ang.helper.SimpleItemTouchHelperCallback
+import com.v2ray.ang.service.V2RayServiceManager
+import com.v2ray.ang.util.*
+import com.v2ray.ang.viewmodel.MainViewModel
+import kotlinx.coroutines.*
+import me.drakeet.support.toast.ToastCompat
+import java.io.File
+import java.io.FileOutputStream
+
+class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedListener {
+ private lateinit var binding: ActivityMainBinding
+
+ private val adapter by lazy { MainRecyclerAdapter(this) }
+ private val mainStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_MAIN, MMKV.MULTI_PROCESS_MODE) }
+ private val settingsStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_SETTING, MMKV.MULTI_PROCESS_MODE) }
+ private val requestVpnPermission = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
+ if (it.resultCode == RESULT_OK) {
+ startV2Ray()
+ }
+ }
+ private var mItemTouchHelper: ItemTouchHelper? = null
+ val mainViewModel: MainViewModel by viewModels()
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ binding = ActivityMainBinding.inflate(layoutInflater)
+ val view = binding.root
+ setContentView(view)
+ title = getString(R.string.title_server)
+ setSupportActionBar(binding.toolbar)
+
+ binding.fab.setOnClickListener {
+ if (mainViewModel.isRunning.value == true) {
+ Utils.stopVService(this)
+ } else if (settingsStorage?.decodeString(AppConfig.PREF_MODE) ?: "VPN" == "VPN") {
+ val intent = VpnService.prepare(this)
+ if (intent == null) {
+ startV2Ray()
+ } else {
+ requestVpnPermission.launch(intent)
+ }
+ } else {
+ startV2Ray()
+ }
+ }
+ binding.layoutTest.setOnClickListener {
+ if (mainViewModel.isRunning.value == true) {
+ setTestState(getString(R.string.connection_test_testing))
+ mainViewModel.testCurrentServerRealPing()
+ } else {
+// tv_test_state.text = getString(R.string.connection_test_fail)
+ }
+ }
+
+ binding.recyclerView.setHasFixedSize(true)
+ binding.recyclerView.layoutManager = LinearLayoutManager(this)
+ binding.recyclerView.adapter = adapter
+
+ val callback = SimpleItemTouchHelperCallback(adapter)
+ mItemTouchHelper = ItemTouchHelper(callback)
+ mItemTouchHelper?.attachToRecyclerView(binding.recyclerView)
+
+
+ val toggle = ActionBarDrawerToggle(
+ this, binding.drawerLayout, binding.toolbar, R.string.navigation_drawer_open, R.string.navigation_drawer_close)
+ binding.drawerLayout.addDrawerListener(toggle)
+ toggle.syncState()
+ binding.navView.setNavigationItemSelectedListener(this)
+ binding.version.text = "v${BuildConfig.VERSION_NAME} (${SpeedtestUtil.getLibVersion()})"
+
+ setupViewModel()
+ copyAssets()
+ migrateLegacy()
+ }
+
+ private fun setupViewModel() {
+ mainViewModel.updateListAction.observe(this) { index ->
+ if (index >= 0) {
+ adapter.notifyItemChanged(index)
+ } else {
+ adapter.notifyDataSetChanged()
+ }
+ }
+ mainViewModel.updateTestResultAction.observe(this) { setTestState(it) }
+ mainViewModel.isRunning.observe(this) { isRunning ->
+ adapter.isRunning = isRunning
+ if (isRunning) {
+ binding.fab.backgroundTintList = ColorStateList.valueOf(ContextCompat.getColor(this, R.color.colorSelected))
+ setTestState(getString(R.string.connection_connected))
+ binding.layoutTest.isFocusable = true
+ } else {
+ binding.fab.backgroundTintList = ColorStateList.valueOf(ContextCompat.getColor(this, R.color.colorUnselected))
+ setTestState(getString(R.string.connection_not_connected))
+ binding.layoutTest.isFocusable = false
+ }
+ hideCircle()
+ }
+ mainViewModel.startListenBroadcast()
+ }
+
+ private fun copyAssets() {
+ val extFolder = Utils.userAssetPath(this)
+ lifecycleScope.launch(Dispatchers.IO) {
+ try {
+ val geo = arrayOf("geosite.dat", "geoip.dat")
+ assets.list("")
+ ?.filter { geo.contains(it) }
+ ?.filter { !File(extFolder, it).exists() }
+ ?.forEach {
+ val target = File(extFolder, it)
+ assets.open(it).use { input ->
+ FileOutputStream(target).use { output ->
+ input.copyTo(output)
+ }
+ }
+ Log.i(ANG_PACKAGE, "Copied from apk assets folder to ${target.absolutePath}")
+ }
+ } catch (e: Exception) {
+ Log.e(ANG_PACKAGE, "asset copy failed", e)
+ }
+ }
+ }
+
+ private fun migrateLegacy() {
+ lifecycleScope.launch(Dispatchers.IO) {
+ val result = AngConfigManager.migrateLegacyConfig(this@MainActivity)
+ if (result != null) {
+ launch(Dispatchers.Main) {
+ if (result) {
+ toast(getString(R.string.migration_success))
+ mainViewModel.reloadServerList()
+ } else {
+ toast(getString(R.string.migration_fail))
+ }
+ }
+ }
+ }
+ }
+
+ fun startV2Ray() {
+ if (mainStorage?.decodeString(MmkvManager.KEY_SELECTED_SERVER).isNullOrEmpty()) {
+ return
+ }
+ showCircle()
+// toast(R.string.toast_services_start)
+ V2RayServiceManager.startV2Ray(this)
+ hideCircle()
+ }
+
+ fun restartV2Ray() {
+ if (mainViewModel.isRunning.value == true) {
+ Utils.stopVService(this)
+ }
+ Observable.timer(500, TimeUnit.MILLISECONDS)
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe {
+ startV2Ray()
+ }
+ }
+
+ public override fun onResume() {
+ super.onResume()
+ mainViewModel.reloadServerList()
+ }
+
+ public override fun onPause() {
+ super.onPause()
+ }
+
+ override fun onCreateOptionsMenu(menu: Menu): Boolean {
+ menuInflater.inflate(R.menu.menu_main, menu)
+ return true
+ }
+
+ override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
+ R.id.import_qrcode -> {
+ importQRcode(true)
+ true
+ }
+ R.id.import_clipboard -> {
+ importClipboard()
+ true
+ }
+ R.id.import_manually_vmess -> {
+ importManually(EConfigType.VMESS.value)
+ true
+ }
+ R.id.import_manually_vless -> {
+ importManually(EConfigType.VLESS.value)
+ true
+ }
+ R.id.import_manually_ss -> {
+ importManually(EConfigType.SHADOWSOCKS.value)
+ true
+ }
+ R.id.import_manually_socks -> {
+ importManually(EConfigType.SOCKS.value)
+ true
+ }
+ R.id.import_manually_trojan -> {
+ importManually(EConfigType.TROJAN.value)
+ true
+ }
+ R.id.import_config_custom_clipboard -> {
+ importConfigCustomClipboard()
+ true
+ }
+ R.id.import_config_custom_local -> {
+ importConfigCustomLocal()
+ true
+ }
+ R.id.import_config_custom_url -> {
+ importConfigCustomUrlClipboard()
+ true
+ }
+ R.id.import_config_custom_url_scan -> {
+ importQRcode(false)
+ true
+ }
+
+// R.id.sub_setting -> {
+// startActivity()
+// true
+// }
+
+ R.id.sub_update -> {
+ importConfigViaSub()
+ true
+ }
+
+ R.id.export_all -> {
+ if (AngConfigManager.shareNonCustomConfigsToClipboard(this, mainViewModel.serverList) == 0) {
+ toast(R.string.toast_success)
+ } else {
+ toast(R.string.toast_failure)
+ }
+ true
+ }
+
+ R.id.ping_all -> {
+ mainViewModel.testAllTcping()
+ true
+ }
+
+ R.id.real_ping_all -> {
+ mainViewModel.testAllRealPing()
+ true
+ }
+
+ R.id.service_restart -> {
+ restartV2Ray()
+ true
+ }
+
+ R.id.del_all_config -> {
+ AlertDialog.Builder(this).setMessage(R.string.del_config_comfirm)
+ .setPositiveButton(android.R.string.ok) { _, _ ->
+ MmkvManager.removeAllServer()
+ mainViewModel.reloadServerList()
+ }
+ .show()
+ true
+ }
+
+ R.id.del_invalid_config -> {
+ AlertDialog.Builder(this).setMessage(R.string.del_config_comfirm)
+ .setPositiveButton(android.R.string.ok) { _, _ ->
+ MmkvManager.removeInvalidServer()
+ mainViewModel.reloadServerList()
+ }
+ .show()
+ true
+ }
+ R.id.sort_by_test_results -> {
+ MmkvManager.sortByTestResults()
+ mainViewModel.reloadServerList()
+ true
+ }
+ R.id.filter_config -> {
+ mainViewModel.filterConfig(this)
+ true
+ }
+
+ else -> super.onOptionsItemSelected(item)
+ }
+
+ private fun importManually(createConfigType : Int) {
+ startActivity(
+ Intent()
+ .putExtra("createConfigType", createConfigType)
+ .putExtra("subscriptionId", mainViewModel.subscriptionId)
+ .setClass(this, ServerActivity::class.java)
+ )
+ }
+
+ /**
+ * import config from qrcode
+ */
+ fun importQRcode(forConfig: Boolean): Boolean {
+// try {
+// startActivityForResult(Intent("com.google.zxing.client.android.SCAN")
+// .addCategory(Intent.CATEGORY_DEFAULT)
+// .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP), requestCode)
+// } catch (e: Exception) {
+ RxPermissions(this)
+ .request(Manifest.permission.CAMERA)
+ .subscribe {
+ if (it)
+ if (forConfig)
+ scanQRCodeForConfig.launch(Intent(this, ScannerActivity::class.java))
+ else
+ scanQRCodeForUrlToCustomConfig.launch(Intent(this, ScannerActivity::class.java))
+ else
+ toast(R.string.toast_permission_denied)
+ }
+// }
+ return true
+ }
+
+ private val scanQRCodeForConfig = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
+ if (it.resultCode == RESULT_OK) {
+ importBatchConfig(it.data?.getStringExtra("SCAN_RESULT"))
+ }
+ }
+
+ private val scanQRCodeForUrlToCustomConfig = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
+ if (it.resultCode == RESULT_OK) {
+ importConfigCustomUrl(it.data?.getStringExtra("SCAN_RESULT"))
+ }
+ }
+
+ /**
+ * import config from clipboard
+ */
+ fun importClipboard()
+ : Boolean {
+ try {
+ val clipboard = Utils.getClipboard(this)
+ importBatchConfig(clipboard)
+ } catch (e: Exception) {
+ e.printStackTrace()
+ return false
+ }
+ return true
+ }
+
+ fun importBatchConfig(server: String?, subid: String = "") {
+ val subid2 = if(subid.isNullOrEmpty()){
+ mainViewModel.subscriptionId
+ }else{
+ subid
+ }
+ val append = subid.isNullOrEmpty()
+
+ var count = AngConfigManager.importBatchConfig(server, subid2, append)
+ if (count <= 0) {
+ count = AngConfigManager.importBatchConfig(Utils.decode(server!!), subid2, append)
+ }
+ if (count > 0) {
+ toast(R.string.toast_success)
+ mainViewModel.reloadServerList()
+ } else {
+ toast(R.string.toast_failure)
+ }
+ }
+
+ fun importConfigCustomClipboard()
+ : Boolean {
+ try {
+ val configText = Utils.getClipboard(this)
+ if (TextUtils.isEmpty(configText)) {
+ toast(R.string.toast_none_data_clipboard)
+ return false
+ }
+ importCustomizeConfig(configText)
+ return true
+ } catch (e: Exception) {
+ e.printStackTrace()
+ return false
+ }
+ }
+
+ /**
+ * import config from local config file
+ */
+ fun importConfigCustomLocal(): Boolean {
+ try {
+ showFileChooser()
+ } catch (e: Exception) {
+ e.printStackTrace()
+ return false
+ }
+ return true
+ }
+
+ fun importConfigCustomUrlClipboard()
+ : Boolean {
+ try {
+ val url = Utils.getClipboard(this)
+ if (TextUtils.isEmpty(url)) {
+ toast(R.string.toast_none_data_clipboard)
+ return false
+ }
+ return importConfigCustomUrl(url)
+ } catch (e: Exception) {
+ e.printStackTrace()
+ return false
+ }
+ }
+
+ /**
+ * import config from url
+ */
+ fun importConfigCustomUrl(url: String?): Boolean {
+ try {
+ if (!Utils.isValidUrl(url)) {
+ toast(R.string.toast_invalid_url)
+ return false
+ }
+ lifecycleScope.launch(Dispatchers.IO) {
+ val configText = try {
+ Utils.getUrlContentWithCustomUserAgent(url)
+ } catch (e: Exception) {
+ e.printStackTrace()
+ ""
+ }
+ launch(Dispatchers.Main) {
+ importCustomizeConfig(configText)
+ }
+ }
+ } catch (e: Exception) {
+ e.printStackTrace()
+ return false
+ }
+ return true
+ }
+
+ /**
+ * import config from sub
+ */
+ fun importConfigViaSub()
+ : Boolean {
+ try {
+ toast(R.string.title_sub_update)
+ MmkvManager.decodeSubscriptions().forEach {
+ if (TextUtils.isEmpty(it.first)
+ || TextUtils.isEmpty(it.second.remarks)
+ || TextUtils.isEmpty(it.second.url)
+ ) {
+ return@forEach
+ }
+ if (!it.second.enabled) {
+ return@forEach
+ }
+ val url = Utils.idnToASCII(it.second.url)
+ if (!Utils.isValidUrl(url)) {
+ return@forEach
+ }
+ Log.d(ANG_PACKAGE, url)
+ lifecycleScope.launch(Dispatchers.IO) {
+ val configText = try {
+ Utils.getUrlContentWithCustomUserAgent(url)
+ } catch (e: Exception) {
+ e.printStackTrace()
+ launch(Dispatchers.Main) {
+ toast("\"" + it.second.remarks + "\" " + getString(R.string.toast_failure))
+ }
+ return@launch
+ }
+ launch(Dispatchers.Main) {
+ importBatchConfig(configText, it.first)
+ }
+ }
+ }
+ } catch (e: Exception) {
+ e.printStackTrace()
+ return false
+ }
+ return true
+ }
+
+ /**
+ * show file chooser
+ */
+ private fun showFileChooser() {
+ val intent = Intent(Intent.ACTION_GET_CONTENT)
+ intent.type = "*/*"
+ intent.addCategory(Intent.CATEGORY_OPENABLE)
+
+ try {
+ chooseFileForCustomConfig.launch(Intent.createChooser(intent, getString(R.string.title_file_chooser)))
+ } catch (ex: ActivityNotFoundException) {
+ toast(R.string.toast_require_file_manager)
+ }
+ }
+
+ private val chooseFileForCustomConfig = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
+ val uri = it.data?.data
+ if (it.resultCode == RESULT_OK && uri != null) {
+ readContentFromUri(uri)
+ }
+ }
+
+ /**
+ * read content from uri
+ */
+ private fun readContentFromUri(uri: Uri) {
+ RxPermissions(this)
+ .request(Manifest.permission.READ_EXTERNAL_STORAGE)
+ .subscribe {
+ if (it) {
+ try {
+ contentResolver.openInputStream(uri).use { input ->
+ importCustomizeConfig(input?.bufferedReader()?.readText())
+ }
+ } catch (e: Exception) {
+ e.printStackTrace()
+ }
+ } else
+ toast(R.string.toast_permission_denied)
+ }
+ }
+
+ /**
+ * import customize config
+ */
+ fun importCustomizeConfig(server: String?) {
+ try {
+ if (server == null || TextUtils.isEmpty(server)) {
+ toast(R.string.toast_none_data)
+ return
+ }
+ mainViewModel.appendCustomConfigServer(server)
+ mainViewModel.reloadServerList()
+ toast(R.string.toast_success)
+ //adapter.notifyItemInserted(mainViewModel.serverList.lastIndex)
+ } catch (e: Exception) {
+ ToastCompat.makeText(this, "${getString(R.string.toast_malformed_josn)} ${e.cause?.message}", Toast.LENGTH_LONG).show()
+ e.printStackTrace()
+ return
+ }
+ }
+
+ fun setTestState(content: String?) {
+ binding.tvTestState.text = content
+ }
+
+// val mConnection = object : ServiceConnection {
+// override fun onServiceDisconnected(name: ComponentName?) {
+// }
+//
+// override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
+// sendMsg(AppConfig.MSG_REGISTER_CLIENT, "")
+// }
+// }
+
+ override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean {
+ if (keyCode == KeyEvent.KEYCODE_BACK) {
+ moveTaskToBack(false)
+ return true
+ }
+ return super.onKeyDown(keyCode, event)
+ }
+
+ fun showCircle() {
+ binding.fabProgressCircle.show()
+ }
+
+ fun hideCircle() {
+ try {
+ Observable.timer(300, TimeUnit.MILLISECONDS)
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe {
+ try {
+ if (binding.fabProgressCircle.isShown) {
+ binding.fabProgressCircle.hide()
+ }
+ } catch (e: Exception) {
+ Log.w(ANG_PACKAGE, e)
+ }
+ }
+ } catch (e: Exception) {
+ Log.d(ANG_PACKAGE, e.toString())
+ }
+ }
+
+ override fun onBackPressed() {
+ if (binding.drawerLayout.isDrawerOpen(GravityCompat.START)) {
+ binding.drawerLayout.closeDrawer(GravityCompat.START)
+ } else {
+ super.onBackPressed()
+ }
+ }
+
+ override fun onNavigationItemSelected(item: MenuItem): Boolean {
+ // Handle navigation view item clicks here.
+ when (item.itemId) {
+ //R.id.server_profile -> activityClass = MainActivity::class.java
+ R.id.sub_setting -> {
+ startActivity(Intent(this, SubSettingActivity::class.java))
+ }
+ R.id.settings -> {
+ startActivity(Intent(this, SettingsActivity::class.java)
+ .putExtra("isRunning", mainViewModel.isRunning.value == true))
+ }
+ R.id.user_asset_setting -> {
+ startActivity(Intent(this, UserAssetActivity::class.java))
+ }
+ R.id.feedback -> {
+ Utils.openUri(this, AppConfig.v2rayNGIssues)
+ }
+ R.id.promotion -> {
+ Utils.openUri(this, "${Utils.decode(AppConfig.promotionUrl)}?t=${System.currentTimeMillis()}")
+ }
+ R.id.logcat -> {
+ startActivity(Intent(this, LogcatActivity::class.java))
+ }
+ }
+ binding.drawerLayout.closeDrawer(GravityCompat.START)
+ return true
+ }
+}
diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/MainRecyclerAdapter.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/MainRecyclerAdapter.kt
new file mode 100644
index 00000000..310d3af7
--- /dev/null
+++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/MainRecyclerAdapter.kt
@@ -0,0 +1,260 @@
+package com.v2ray.ang.ui
+
+import android.content.Intent
+import android.graphics.Color
+import android.text.TextUtils
+import androidx.core.content.ContextCompat
+import androidx.recyclerview.widget.RecyclerView
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.appcompat.app.AlertDialog
+import com.google.gson.Gson
+import com.tencent.mmkv.MMKV
+import com.v2ray.ang.AppConfig
+import com.v2ray.ang.R
+import com.v2ray.ang.databinding.ItemQrcodeBinding
+import com.v2ray.ang.databinding.ItemRecyclerFooterBinding
+import com.v2ray.ang.databinding.ItemRecyclerMainBinding
+import com.v2ray.ang.dto.EConfigType
+import com.v2ray.ang.dto.SubscriptionItem
+import com.v2ray.ang.extension.toast
+import com.v2ray.ang.helper.ItemTouchHelperAdapter
+import com.v2ray.ang.helper.ItemTouchHelperViewHolder
+import com.v2ray.ang.service.V2RayServiceManager
+import com.v2ray.ang.util.AngConfigManager
+import com.v2ray.ang.util.MmkvManager
+import com.v2ray.ang.util.Utils
+import rx.Observable
+import rx.android.schedulers.AndroidSchedulers
+import java.util.concurrent.TimeUnit
+
+class MainRecyclerAdapter(val activity: MainActivity) : RecyclerView.Adapter()
+ , ItemTouchHelperAdapter {
+ companion object {
+ private const val VIEW_TYPE_ITEM = 1
+ private const val VIEW_TYPE_FOOTER = 2
+ }
+
+ private var mActivity: MainActivity = activity
+ private val mainStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_MAIN, MMKV.MULTI_PROCESS_MODE) }
+ private val subStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_SUB, MMKV.MULTI_PROCESS_MODE) }
+ private val settingsStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_SETTING, MMKV.MULTI_PROCESS_MODE) }
+ private val share_method: Array by lazy {
+ mActivity.resources.getStringArray(R.array.share_method)
+ }
+ var isRunning = false
+
+ override fun getItemCount() = mActivity.mainViewModel.serversCache.size + 1
+
+ override fun onBindViewHolder(holder: BaseViewHolder, position: Int) {
+ if (holder is MainViewHolder) {
+ val guid = mActivity.mainViewModel.serversCache[position].guid
+ val config = mActivity.mainViewModel.serversCache[position].config
+// //filter
+// if (mActivity.mainViewModel.subscriptionId.isNotEmpty()
+// && mActivity.mainViewModel.subscriptionId != config.subscriptionId
+// ) {
+// holder.itemMainBinding.cardView.visibility = View.GONE
+// } else {
+// holder.itemMainBinding.cardView.visibility = View.VISIBLE
+// }
+
+ val outbound = config.getProxyOutbound()
+ val aff = MmkvManager.decodeServerAffiliationInfo(guid)
+
+ holder.itemMainBinding.tvName.text = config.remarks
+ holder.itemView.setBackgroundColor(Color.TRANSPARENT)
+ holder.itemMainBinding.tvTestResult.text = aff?.getTestDelayString() ?: ""
+ if ((aff?.testDelayMillis ?: 0L) < 0L) {
+ holder.itemMainBinding.tvTestResult.setTextColor(ContextCompat.getColor(mActivity, R.color.colorPingRed))
+ } else {
+ holder.itemMainBinding.tvTestResult.setTextColor(ContextCompat.getColor(mActivity, R.color.colorPing))
+ }
+ if (guid == mainStorage?.decodeString(MmkvManager.KEY_SELECTED_SERVER)) {
+ holder.itemMainBinding.layoutIndicator.setBackgroundResource(R.color.colorSelected)
+ } else {
+ holder.itemMainBinding.layoutIndicator.setBackgroundResource(R.color.colorUnselected)
+ }
+ holder.itemMainBinding.tvSubscription.text = ""
+ val json = subStorage?.decodeString(config.subscriptionId)
+ if (!json.isNullOrBlank()) {
+ val sub = Gson().fromJson(json, SubscriptionItem::class.java)
+ holder.itemMainBinding.tvSubscription.text = sub.remarks
+ }
+
+ var shareOptions = share_method.asList()
+ when (config.configType) {
+ EConfigType.CUSTOM -> {
+ holder.itemMainBinding.tvType.text = mActivity.getString(R.string.server_customize_config)
+ shareOptions = shareOptions.takeLast(1)
+ }
+ EConfigType.VLESS -> {
+ holder.itemMainBinding.tvType.text = config.configType.name
+ }
+ else -> {
+ holder.itemMainBinding.tvType.text = config.configType.name.lowercase()
+ }
+ }
+ holder.itemMainBinding.tvStatistics.text = "${outbound?.getServerAddress()} : ${outbound?.getServerPort()}"
+
+ holder.itemMainBinding.layoutShare.setOnClickListener {
+ AlertDialog.Builder(mActivity).setItems(shareOptions.toTypedArray()) { _, i ->
+ try {
+ when (i) {
+ 0 -> {
+ if (config.configType == EConfigType.CUSTOM) {
+ shareFullContent(guid)
+ } else {
+ val ivBinding = ItemQrcodeBinding.inflate(LayoutInflater.from(mActivity))
+ ivBinding.ivQcode.setImageBitmap(AngConfigManager.share2QRCode(guid))
+ AlertDialog.Builder(mActivity).setView(ivBinding.root).show()
+ }
+ }
+ 1 -> {
+ if (AngConfigManager.share2Clipboard(mActivity, guid) == 0) {
+ mActivity.toast(R.string.toast_success)
+ } else {
+ mActivity.toast(R.string.toast_failure)
+ }
+ }
+ 2 -> shareFullContent(guid)
+ else -> mActivity.toast("else")
+ }
+ } catch (e: Exception) {
+ e.printStackTrace()
+ }
+ }.show()
+ }
+
+ holder.itemMainBinding.layoutEdit.setOnClickListener {
+ val intent = Intent().putExtra("guid", guid)
+ .putExtra("isRunning", isRunning)
+ if (config.configType == EConfigType.CUSTOM) {
+ mActivity.startActivity(intent.setClass(mActivity, ServerCustomConfigActivity::class.java))
+ } else {
+ mActivity.startActivity(intent.setClass(mActivity, ServerActivity::class.java))
+ }
+ }
+ holder.itemMainBinding.layoutRemove.setOnClickListener {
+ if (guid != mainStorage?.decodeString(MmkvManager.KEY_SELECTED_SERVER)) {
+ if (settingsStorage?.decodeBool(AppConfig.PREF_CONFIRM_REMOVE) == true) {
+ AlertDialog.Builder(mActivity).setMessage(R.string.del_config_comfirm)
+ .setPositiveButton(android.R.string.ok) { _, _ ->
+ removeServer(guid, position)
+ }
+ .show()
+ } else {
+ removeServer(guid, position)
+ }
+ }
+ }
+
+ holder.itemMainBinding.infoContainer.setOnClickListener {
+ val selected = mainStorage?.decodeString(MmkvManager.KEY_SELECTED_SERVER)
+ if (guid != selected) {
+ mainStorage?.encode(MmkvManager.KEY_SELECTED_SERVER, guid)
+ if (!TextUtils.isEmpty(selected)) {
+ notifyItemChanged(mActivity.mainViewModel.getPosition(selected!!))
+ }
+ notifyItemChanged(mActivity.mainViewModel.getPosition(guid))
+ if (isRunning) {
+ mActivity.showCircle()
+ Utils.stopVService(mActivity)
+ Observable.timer(500, TimeUnit.MILLISECONDS)
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe {
+ V2RayServiceManager.startV2Ray(mActivity)
+ mActivity.hideCircle()
+ }
+ }
+ }
+ }
+ }
+ if (holder is FooterViewHolder) {
+ //if (activity?.defaultDPreference?.getPrefBoolean(AppConfig.PREF_INAPP_BUY_IS_PREMIUM, false)) {
+ if (true) {
+ holder.itemFooterBinding.layoutEdit.visibility = View.INVISIBLE
+ } else {
+ holder.itemFooterBinding.layoutEdit.setOnClickListener {
+ Utils.openUri(mActivity, "${Utils.decode(AppConfig.promotionUrl)}?t=${System.currentTimeMillis()}")
+ }
+ }
+ }
+ }
+
+ private fun shareFullContent(guid: String) {
+ if (AngConfigManager.shareFullContent2Clipboard(mActivity, guid) == 0) {
+ mActivity.toast(R.string.toast_success)
+ } else {
+ mActivity.toast(R.string.toast_failure)
+ }
+ }
+
+ private fun removeServer(guid: String,position:Int) {
+ mActivity.mainViewModel.removeServer(guid)
+ notifyItemRemoved(position)
+ notifyItemRangeChanged(position, mActivity.mainViewModel.serversCache.size)
+ }
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseViewHolder {
+ return when (viewType) {
+ VIEW_TYPE_ITEM ->
+ MainViewHolder(ItemRecyclerMainBinding.inflate(LayoutInflater.from(parent.context), parent, false))
+ else ->
+ FooterViewHolder(ItemRecyclerFooterBinding.inflate(LayoutInflater.from(parent.context), parent, false))
+ }
+ }
+
+ override fun getItemViewType(position: Int): Int {
+ return if (position == mActivity.mainViewModel.serversCache.size) {
+ VIEW_TYPE_FOOTER
+ } else {
+ VIEW_TYPE_ITEM
+ }
+ }
+
+ open class BaseViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
+ fun onItemSelected() {
+ itemView.setBackgroundColor(Color.LTGRAY)
+ }
+
+ fun onItemClear() {
+ itemView.setBackgroundColor(0)
+ }
+ }
+
+ class MainViewHolder(val itemMainBinding: ItemRecyclerMainBinding) :
+ BaseViewHolder(itemMainBinding.root), ItemTouchHelperViewHolder
+
+ class FooterViewHolder(val itemFooterBinding: ItemRecyclerFooterBinding) :
+ BaseViewHolder(itemFooterBinding.root), ItemTouchHelperViewHolder
+
+ override fun onItemDismiss(position: Int) {
+ val guid = mActivity.mainViewModel.serversCache.getOrNull(position)?.guid ?: return
+ if (guid != mainStorage?.decodeString(MmkvManager.KEY_SELECTED_SERVER)) {
+// mActivity.alert(R.string.del_config_comfirm) {
+// positiveButton(android.R.string.ok) {
+ mActivity.mainViewModel.removeServer(guid)
+ notifyItemRemoved(position)
+// }
+// show()
+// }
+ }
+ }
+
+ override fun onItemMove(fromPosition: Int, toPosition: Int): Boolean {
+ mActivity.mainViewModel.swapServer(fromPosition, toPosition)
+ notifyItemMoved(fromPosition, toPosition)
+ // position is changed, since position is used by click callbacks, need to update range
+ if (toPosition > fromPosition)
+ notifyItemRangeChanged(fromPosition, toPosition - fromPosition + 1)
+ else
+ notifyItemRangeChanged(toPosition, fromPosition - toPosition + 1)
+ return true
+ }
+
+ override fun onItemMoveCompleted() {
+ // do nothing
+ }
+}
diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/PerAppProxyActivity.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/PerAppProxyActivity.kt
new file mode 100644
index 00000000..eabed93d
--- /dev/null
+++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/PerAppProxyActivity.kt
@@ -0,0 +1,355 @@
+package com.v2ray.ang.ui
+
+import android.os.Bundle
+import android.text.TextUtils
+import android.util.Log
+import android.view.Menu
+import android.view.MenuItem
+import android.view.View
+import androidx.appcompat.widget.SearchView
+import androidx.lifecycle.lifecycleScope
+import androidx.preference.PreferenceManager
+import androidx.recyclerview.widget.DividerItemDecoration
+import androidx.recyclerview.widget.LinearLayoutManager
+import com.v2ray.ang.AppConfig
+import com.v2ray.ang.AppConfig.ANG_PACKAGE
+import com.v2ray.ang.R
+import com.v2ray.ang.databinding.ActivityBypassListBinding
+import com.v2ray.ang.dto.AppInfo
+import com.v2ray.ang.extension.toast
+import com.v2ray.ang.extension.v2RayApplication
+import com.v2ray.ang.util.AppManagerUtil
+import com.v2ray.ang.util.Utils
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.GlobalScope
+import kotlinx.coroutines.launch
+import rx.android.schedulers.AndroidSchedulers
+import rx.schedulers.Schedulers
+import java.text.Collator
+import java.util.*
+
+class PerAppProxyActivity : BaseActivity() {
+ private lateinit var binding: ActivityBypassListBinding
+
+ private var adapter: PerAppProxyAdapter? = null
+ private var appsAll: List? = null
+ private val defaultSharedPreferences by lazy { PreferenceManager.getDefaultSharedPreferences(this) }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ binding = ActivityBypassListBinding.inflate(layoutInflater)
+ val view = binding.root
+ setContentView(view)
+
+ supportActionBar?.setDisplayHomeAsUpEnabled(true)
+
+ val dividerItemDecoration = DividerItemDecoration(this, LinearLayoutManager.VERTICAL)
+ binding.recyclerView.addItemDecoration(dividerItemDecoration)
+
+ val blacklist = defaultSharedPreferences.getStringSet(AppConfig.PREF_PER_APP_PROXY_SET, null)
+
+ AppManagerUtil.rxLoadNetworkAppList(this)
+ .subscribeOn(Schedulers.io())
+ .map {
+ if (blacklist != null) {
+ it.forEach { one ->
+ if ((blacklist.contains(one.packageName))) {
+ one.isSelected = 1
+ } else {
+ one.isSelected = 0
+ }
+ }
+ val comparator = Comparator { p1, p2 ->
+ when {
+ p1.isSelected > p2.isSelected -> -1
+ p1.isSelected == p2.isSelected -> 0
+ else -> 1
+ }
+ }
+ it.sortedWith(comparator)
+ } else {
+ val comparator = object : Comparator {
+ val collator = Collator.getInstance()
+ override fun compare(o1: AppInfo, o2: AppInfo) = collator.compare(o1.appName, o2.appName)
+ }
+ it.sortedWith(comparator)
+ }
+ }
+// .map {
+// val comparator = object : Comparator {
+// val collator = Collator.getInstance()
+// override fun compare(o1: AppInfo, o2: AppInfo) = collator.compare(o1.appName, o2.appName)
+// }
+// it.sortedWith(comparator)
+// }
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe {
+ appsAll = it
+ adapter = PerAppProxyAdapter(this, it, blacklist)
+ binding.recyclerView.adapter = adapter
+ binding.pbWaiting.visibility = View.GONE
+ }
+ /***
+ recycler_view.addOnScrollListener(object : RecyclerView.OnScrollListener() {
+ var dst = 0
+ val threshold = resources.getDimensionPixelSize(R.dimen.bypass_list_header_height) * 2
+ override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
+ dst += dy
+ if (dst > threshold) {
+ header_view.hide()
+ dst = 0
+ } else if (dst < -20) {
+ header_view.show()
+ dst = 0
+ }
+ }
+
+ var hiding = false
+ fun View.hide() {
+ val target = -height.toFloat()
+ if (hiding || translationY == target) return
+ animate()
+ .translationY(target)
+ .setInterpolator(AccelerateInterpolator(2F))
+ .setListener(object : AnimatorListenerAdapter() {
+ override fun onAnimationEnd(animation: Animator?) {
+ hiding = false
+ }
+ })
+ hiding = true
+ }
+
+ var showing = false
+ fun View.show() {
+ val target = 0f
+ if (showing || translationY == target) return
+ animate()
+ .translationY(target)
+ .setInterpolator(DecelerateInterpolator(2F))
+ .setListener(object : AnimatorListenerAdapter() {
+ override fun onAnimationEnd(animation: Animator?) {
+ showing = false
+ }
+ })
+ showing = true
+ }
+ })
+ ***/
+
+ binding.switchPerAppProxy.setOnCheckedChangeListener { _, isChecked ->
+ defaultSharedPreferences.edit().putBoolean(AppConfig.PREF_PER_APP_PROXY, isChecked).apply()
+ }
+ binding.switchPerAppProxy.isChecked = defaultSharedPreferences.getBoolean(AppConfig.PREF_PER_APP_PROXY, false)
+
+ binding.switchBypassApps.setOnCheckedChangeListener { _, isChecked ->
+ defaultSharedPreferences.edit().putBoolean(AppConfig.PREF_BYPASS_APPS, isChecked).apply()
+ }
+ binding.switchBypassApps.isChecked = defaultSharedPreferences.getBoolean(AppConfig.PREF_BYPASS_APPS, false)
+
+ /***
+ et_search.setOnEditorActionListener { v, actionId, event ->
+ if (actionId == EditorInfo.IME_ACTION_SEARCH) {
+ //hide
+ var imm: InputMethodManager = v.context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
+ imm.toggleSoftInput(0, InputMethodManager.HIDE_NOT_ALWAYS)
+
+ val key = v.text.toString().toUpperCase()
+ val apps = ArrayList()
+ if (TextUtils.isEmpty(key)) {
+ appsAll?.forEach {
+ apps.add(it)
+ }
+ } else {
+ appsAll?.forEach {
+ if (it.appName.toUpperCase().indexOf(key) >= 0) {
+ apps.add(it)
+ }
+ }
+ }
+ adapter = PerAppProxyAdapter(this, apps, adapter?.blacklist)
+ recycler_view.adapter = adapter
+ adapter?.notifyDataSetChanged()
+ true
+ } else {
+ false
+ }
+ }
+ ***/
+ }
+
+ override fun onPause() {
+ super.onPause()
+ adapter?.let {
+ defaultSharedPreferences.edit().putStringSet(AppConfig.PREF_PER_APP_PROXY_SET, it.blacklist).apply()
+ }
+ }
+
+ override fun onCreateOptionsMenu(menu: Menu): Boolean {
+ menuInflater.inflate(R.menu.menu_bypass_list, menu)
+
+ val searchItem = menu.findItem(R.id.search_view)
+ if (searchItem != null) {
+ val searchView = searchItem.actionView as SearchView
+ searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
+ override fun onQueryTextSubmit(query: String?): Boolean {
+ return false
+ }
+
+ override fun onQueryTextChange(newText: String?): Boolean {
+ filterProxyApp(newText!!)
+ return false
+ }
+ })
+ }
+
+
+ return super.onCreateOptionsMenu(menu)
+ }
+
+ override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
+ R.id.select_all -> adapter?.let {
+ val pkgNames = it.apps.map { it.packageName }
+ if (it.blacklist.containsAll(pkgNames)) {
+ it.apps.forEach {
+ val packageName = it.packageName
+ adapter?.blacklist!!.remove(packageName)
+ }
+ } else {
+ it.apps.forEach {
+ val packageName = it.packageName
+ adapter?.blacklist!!.add(packageName)
+ }
+ }
+ it.notifyDataSetChanged()
+ true
+ } ?: false
+ R.id.select_proxy_app -> {
+ selectProxyApp()
+ true
+ }
+ R.id.import_proxy_app -> {
+ importProxyApp()
+ true
+ }
+ R.id.export_proxy_app -> {
+ exportProxyApp()
+ true
+ }
+ else -> super.onOptionsItemSelected(item)
+ }
+
+ private fun selectProxyApp() {
+ toast(R.string.msg_downloading_content)
+ val url = AppConfig.androidpackagenamelistUrl
+ lifecycleScope.launch(Dispatchers.IO) {
+ val content = Utils.getUrlContext(url, 5000)
+ launch(Dispatchers.Main) {
+ Log.d(ANG_PACKAGE, content)
+ selectProxyApp(content, true)
+ toast(R.string.toast_success)
+ }
+ }
+ }
+
+ private fun importProxyApp() {
+ val content = Utils.getClipboard(applicationContext)
+ if (TextUtils.isEmpty(content)) {
+ return
+ }
+ selectProxyApp(content, false)
+ toast(R.string.toast_success)
+ }
+
+ private fun exportProxyApp() {
+ var lst = binding.switchBypassApps.isChecked.toString()
+
+ adapter?.blacklist?.forEach block@{
+ lst = lst + System.getProperty("line.separator") + it
+ }
+ Utils.setClipboard(applicationContext, lst)
+ toast(R.string.toast_success)
+ }
+
+ private fun selectProxyApp(content: String, force: Boolean): Boolean {
+ try {
+ val proxyApps = if (TextUtils.isEmpty(content)) {
+ Utils.readTextFromAssets(v2RayApplication, "proxy_packagename.txt")
+ } else {
+ content
+ }
+ if (TextUtils.isEmpty(proxyApps)) {
+ return false
+ }
+
+ adapter?.blacklist!!.clear()
+
+ if (binding.switchBypassApps.isChecked) {
+ adapter?.let {
+ it.apps.forEach block@{
+ val packageName = it.packageName
+ Log.d(ANG_PACKAGE, packageName)
+ if (!inProxyApps(proxyApps, packageName, force)) {
+ adapter?.blacklist!!.add(packageName)
+ println(packageName)
+ return@block
+ }
+ }
+ it.notifyDataSetChanged()
+ }
+ } else {
+ adapter?.let {
+ it.apps.forEach block@{
+ val packageName = it.packageName
+ Log.d(ANG_PACKAGE, packageName)
+ if (inProxyApps(proxyApps, packageName, force)) {
+ adapter?.blacklist!!.add(packageName)
+ println(packageName)
+ return@block
+ }
+ }
+ it.notifyDataSetChanged()
+ }
+ }
+ } catch (e: Exception) {
+ e.printStackTrace()
+ return false
+ }
+ return true
+ }
+
+ private fun inProxyApps(proxyApps: String, packageName: String, force: Boolean): Boolean {
+ if (force) {
+ if (packageName == "com.google.android.webview") {
+ return false
+ }
+ if (packageName.startsWith("com.google")) {
+ return true
+ }
+ }
+
+ return proxyApps.indexOf(packageName) >= 0
+ }
+
+ private fun filterProxyApp(content: String): Boolean {
+ val apps = ArrayList()
+
+ val key = content.uppercase()
+ if (key.isNotEmpty()) {
+ appsAll?.forEach {
+ if (it.appName.uppercase().indexOf(key) >= 0
+ || it.packageName.uppercase().indexOf(key) >= 0) {
+ apps.add(it)
+ }
+ }
+ } else {
+ appsAll?.forEach {
+ apps.add(it)
+ }
+ }
+
+ adapter = PerAppProxyAdapter(this, apps, adapter?.blacklist)
+ binding.recyclerView.adapter = adapter
+ adapter?.notifyDataSetChanged()
+ return true
+ }
+}
diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/PerAppProxyAdapter.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/PerAppProxyAdapter.kt
new file mode 100644
index 00000000..040e9b9c
--- /dev/null
+++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/PerAppProxyAdapter.kt
@@ -0,0 +1,87 @@
+package com.v2ray.ang.ui
+
+import android.view.LayoutInflater
+import androidx.recyclerview.widget.RecyclerView
+import android.view.View
+import android.view.ViewGroup
+import com.v2ray.ang.R
+import com.v2ray.ang.databinding.ItemRecyclerBypassListBinding
+import com.v2ray.ang.dto.AppInfo
+import java.util.*
+
+class PerAppProxyAdapter(val activity: BaseActivity, val apps: List, blacklist: MutableSet?) :
+ RecyclerView.Adapter() {
+
+ companion object {
+ private const val VIEW_TYPE_HEADER = 0
+ private const val VIEW_TYPE_ITEM = 1
+ }
+
+ val blacklist = if (blacklist == null) HashSet() else HashSet(blacklist)
+
+ override fun onBindViewHolder(holder: BaseViewHolder, position: Int) {
+ if (holder is AppViewHolder) {
+ val appInfo = apps[position - 1]
+ holder.bind(appInfo)
+ }
+ }
+
+ override fun getItemCount() = apps.size + 1
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseViewHolder {
+ val ctx = parent.context
+
+ return when (viewType) {
+ VIEW_TYPE_HEADER -> {
+ val view = View(ctx)
+ view.layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
+ ctx.resources.getDimensionPixelSize(R.dimen.bypass_list_header_height) * 0)
+ BaseViewHolder(view)
+ }
+// VIEW_TYPE_ITEM -> AppViewHolder(ctx.layoutInflater
+// .inflate(R.layout.item_recycler_bypass_list, parent, false))
+
+ else -> AppViewHolder(ItemRecyclerBypassListBinding.inflate(LayoutInflater.from(ctx), parent, false))
+
+ }
+ }
+
+ override fun getItemViewType(position: Int) = if (position == 0) VIEW_TYPE_HEADER else VIEW_TYPE_ITEM
+
+ open class BaseViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView)
+
+ inner class AppViewHolder(private val itemBypassBinding: ItemRecyclerBypassListBinding) : BaseViewHolder(itemBypassBinding.root),
+ View.OnClickListener {
+ private val inBlacklist: Boolean get() = blacklist.contains(appInfo.packageName)
+ private lateinit var appInfo: AppInfo
+
+ fun bind(appInfo: AppInfo) {
+ this.appInfo = appInfo
+
+ itemBypassBinding.icon.setImageDrawable(appInfo.appIcon)
+// name.text = appInfo.appName
+
+ itemBypassBinding.checkBox.isChecked = inBlacklist
+ itemBypassBinding.packageName.text = appInfo.packageName
+ if (appInfo.isSystemApp) {
+ itemBypassBinding.name.text = String.format("** %1s", appInfo.appName)
+ //name.textColor = Color.RED
+ } else {
+ itemBypassBinding.name.text = appInfo.appName
+ //name.textColor = Color.DKGRAY
+ }
+
+ itemView.setOnClickListener(this)
+ }
+
+ override fun onClick(v: View?) {
+ if (inBlacklist) {
+ blacklist.remove(appInfo.packageName)
+ itemBypassBinding.checkBox.isChecked = false
+ } else {
+ blacklist.add(appInfo.packageName)
+ itemBypassBinding.checkBox.isChecked = true
+ }
+ }
+ }
+}
diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/RoutingSettingsActivity.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/RoutingSettingsActivity.kt
new file mode 100644
index 00000000..030e80ff
--- /dev/null
+++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/RoutingSettingsActivity.kt
@@ -0,0 +1,38 @@
+package com.v2ray.ang.ui
+
+import android.os.Bundle
+import com.v2ray.ang.R
+import androidx.fragment.app.Fragment
+import com.google.android.material.tabs.TabLayoutMediator
+import com.v2ray.ang.AppConfig
+import com.v2ray.ang.databinding.ActivityRoutingSettingsBinding
+
+class RoutingSettingsActivity : BaseActivity() {
+ private lateinit var binding: ActivityRoutingSettingsBinding
+
+ private val titles: Array by lazy {
+ resources.getStringArray(R.array.routing_tag)
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ binding = ActivityRoutingSettingsBinding.inflate(layoutInflater)
+ val view = binding.root
+ setContentView(view)
+
+ title = getString(R.string.title_pref_routing_custom)
+ supportActionBar?.setDisplayHomeAsUpEnabled(true)
+
+ val fragments = ArrayList()
+ fragments.add(RoutingSettingsFragment().newInstance(AppConfig.PREF_V2RAY_ROUTING_AGENT))
+ fragments.add(RoutingSettingsFragment().newInstance(AppConfig.PREF_V2RAY_ROUTING_DIRECT))
+ fragments.add(RoutingSettingsFragment().newInstance(AppConfig.PREF_V2RAY_ROUTING_BLOCKED))
+
+ val adapter = FragmentAdapter(this, fragments)
+ binding.viewpager.adapter = adapter
+ //tablayout.setTabTextColors(Color.BLACK, Color.RED)
+ TabLayoutMediator(binding.tablayout, binding.viewpager) { tab, position ->
+ tab.text = titles[position]
+ }.attach()
+ }
+}
diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/RoutingSettingsFragment.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/RoutingSettingsFragment.kt
new file mode 100644
index 00000000..f5a8e395
--- /dev/null
+++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/RoutingSettingsFragment.kt
@@ -0,0 +1,158 @@
+package com.v2ray.ang.ui
+
+import android.Manifest
+import android.app.Activity.RESULT_OK
+import android.content.Intent
+import android.os.Bundle
+import android.text.TextUtils
+import android.view.*
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.fragment.app.Fragment
+import androidx.lifecycle.lifecycleScope
+import androidx.preference.PreferenceManager
+import com.tbruyelle.rxpermissions.RxPermissions
+import com.v2ray.ang.AppConfig
+import com.v2ray.ang.R
+import com.v2ray.ang.databinding.FragmentRoutingSettingsBinding
+import com.v2ray.ang.extension.toast
+import com.v2ray.ang.extension.v2RayApplication
+import com.v2ray.ang.util.Utils
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.GlobalScope
+import kotlinx.coroutines.launch
+
+class RoutingSettingsFragment : Fragment() {
+ private lateinit var binding: FragmentRoutingSettingsBinding
+ companion object {
+ private const val routing_arg = "routing_arg"
+ }
+
+ val defaultSharedPreferences by lazy { PreferenceManager.getDefaultSharedPreferences(requireContext()) }
+
+ override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
+ savedInstanceState: Bundle?): View? {
+ // Inflate the layout for this fragment
+ binding = FragmentRoutingSettingsBinding.inflate(layoutInflater)
+ return binding.root// inflater.inflate(R.layout.fragment_routing_settings, container, false)
+ }
+
+ fun newInstance(arg: String): Fragment {
+ val fragment = RoutingSettingsFragment()
+ val bundle = Bundle()
+ bundle.putString(routing_arg, arg)
+ fragment.arguments = bundle
+ return fragment
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+
+ val content = defaultSharedPreferences.getString(requireArguments().getString(routing_arg), "")
+ binding.etRoutingContent.text = Utils.getEditable(content!!)
+
+ setHasOptionsMenu(true)
+ }
+
+ override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
+ inflater.inflate(R.menu.menu_routing, menu)
+ return super.onCreateOptionsMenu(menu, inflater)
+ }
+
+ override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
+ R.id.save_routing -> {
+ saveRouting()
+ true
+ }
+ R.id.del_routing -> {
+ binding.etRoutingContent.text = null
+ true
+ }
+ R.id.scan_replace -> {
+ scanQRcode(true)
+ true
+ }
+ R.id.scan_append -> {
+ scanQRcode(false)
+ true
+ }
+ R.id.default_rules -> {
+ setDefaultRules()
+ true
+ }
+ else -> super.onOptionsItemSelected(item)
+ }
+
+ private fun saveRouting() {
+ val content = binding.etRoutingContent.text.toString()
+ defaultSharedPreferences.edit().putString(requireArguments().getString(routing_arg), content).apply()
+ activity?.toast(R.string.toast_success)
+ }
+
+ fun scanQRcode(forReplace: Boolean): Boolean {
+// try {
+// startActivityForResult(Intent("com.google.zxing.client.android.SCAN")
+// .addCategory(Intent.CATEGORY_DEFAULT)
+// .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP), requestCode)
+// } catch (e: Exception) {
+ RxPermissions(requireActivity())
+ .request(Manifest.permission.CAMERA)
+ .subscribe {
+ if (it)
+ if (forReplace)
+ scanQRCodeForReplace.launch(Intent(activity, ScannerActivity::class.java))
+ else
+ scanQRCodeForAppend.launch(Intent(activity, ScannerActivity::class.java))
+ else
+ activity?.toast(R.string.toast_permission_denied)
+ }
+// }
+ return true
+ }
+
+ private val scanQRCodeForReplace = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
+ if (it.resultCode == RESULT_OK) {
+ val content = it.data?.getStringExtra("SCAN_RESULT")
+ binding.etRoutingContent.text = Utils.getEditable(content!!)
+ }
+ }
+
+ private val scanQRCodeForAppend = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
+ if (it.resultCode == RESULT_OK) {
+ val content = it.data?.getStringExtra("SCAN_RESULT")
+ binding.etRoutingContent.text = Utils.getEditable("${binding.etRoutingContent.text},$content")
+ }
+ }
+
+ fun setDefaultRules(): Boolean {
+ var url = AppConfig.v2rayCustomRoutingListUrl
+ var tag = ""
+ when (requireArguments().getString(routing_arg)) {
+ AppConfig.PREF_V2RAY_ROUTING_AGENT -> {
+ tag = AppConfig.TAG_AGENT
+ }
+ AppConfig.PREF_V2RAY_ROUTING_DIRECT -> {
+ tag = AppConfig.TAG_DIRECT
+ }
+ AppConfig.PREF_V2RAY_ROUTING_BLOCKED -> {
+ tag = AppConfig.TAG_BLOCKED
+ }
+ }
+ url += tag
+
+ activity?.toast(R.string.msg_downloading_content)
+ lifecycleScope.launch(Dispatchers.IO) {
+ val content = Utils.getUrlContext(url, 5000)
+ launch(Dispatchers.Main) {
+ val routingList = if (TextUtils.isEmpty(content)) {
+ Utils.readTextFromAssets(activity?.v2RayApplication!!, "custom_routing_$tag")
+ } else {
+ content
+ }
+ binding.etRoutingContent.text = Utils.getEditable(routingList)
+ saveRouting()
+ //toast(R.string.toast_success)
+ }
+ }
+ return true
+ }
+}
diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/ScScannerActivity.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/ScScannerActivity.kt
new file mode 100644
index 00000000..da1cb7cd
--- /dev/null
+++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/ScScannerActivity.kt
@@ -0,0 +1,45 @@
+package com.v2ray.ang.ui
+
+import android.Manifest
+import android.content.*
+import com.tbruyelle.rxpermissions.RxPermissions
+import com.v2ray.ang.R
+import com.v2ray.ang.util.AngConfigManager
+import android.os.Bundle
+import androidx.activity.result.contract.ActivityResultContracts
+import com.v2ray.ang.extension.toast
+
+class ScScannerActivity : BaseActivity() {
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_none)
+ importQRcode()
+ }
+
+ fun importQRcode(): Boolean {
+ RxPermissions(this)
+ .request(Manifest.permission.CAMERA)
+ .subscribe {
+ if (it)
+ scanQRCode.launch(Intent(this, ScannerActivity::class.java))
+ else
+ toast(R.string.toast_permission_denied)
+ }
+
+ return true
+ }
+
+ private val scanQRCode = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
+ if (it.resultCode == RESULT_OK) {
+ val count = AngConfigManager.importBatchConfig(it.data?.getStringExtra("SCAN_RESULT"), "", false)
+ if (count > 0) {
+ toast(R.string.toast_success)
+ } else {
+ toast(R.string.toast_failure)
+ }
+ startActivity(Intent(this, MainActivity::class.java))
+ }
+ finish()
+ }
+}
diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/ScSwitchActivity.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/ScSwitchActivity.kt
new file mode 100644
index 00000000..3d3101ef
--- /dev/null
+++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/ScSwitchActivity.kt
@@ -0,0 +1,22 @@
+package com.v2ray.ang.ui
+
+import com.v2ray.ang.R
+import com.v2ray.ang.util.Utils
+import android.os.Bundle
+import com.v2ray.ang.service.V2RayServiceManager
+
+class ScSwitchActivity : BaseActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ moveTaskToBack(true)
+
+ setContentView(R.layout.activity_none)
+
+ if (V2RayServiceManager.v2rayPoint.isRunning) {
+ Utils.stopVService(this)
+ } else {
+ Utils.startVServiceFromToggle(this)
+ }
+ finish()
+ }
+}
diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/ScannerActivity.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/ScannerActivity.kt
new file mode 100644
index 00000000..bc3785ec
--- /dev/null
+++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/ScannerActivity.kt
@@ -0,0 +1,116 @@
+package com.v2ray.ang.ui
+
+import android.Manifest
+import android.app.Activity
+import android.os.Bundle
+import com.google.zxing.Result
+import me.dm7.barcodescanner.zxing.ZXingScannerView
+import android.content.Intent
+import android.graphics.BitmapFactory
+import android.view.Menu
+import android.view.MenuItem
+import androidx.activity.result.contract.ActivityResultContracts
+import com.google.zxing.BarcodeFormat
+import com.tbruyelle.rxpermissions.RxPermissions
+import com.v2ray.ang.R
+import com.v2ray.ang.extension.toast
+import com.v2ray.ang.util.QRCodeDecoder
+
+class ScannerActivity : BaseActivity(), ZXingScannerView.ResultHandler {
+
+ private var mScannerView: ZXingScannerView? = null
+
+ public override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ mScannerView = ZXingScannerView(this) // Programmatically initialize the scanner view
+
+ mScannerView?.setAutoFocus(true)
+ val formats = ArrayList()
+ formats.add(BarcodeFormat.QR_CODE)
+ mScannerView?.setFormats(formats)
+
+ setContentView(mScannerView) // Set the scanner view as the content view
+
+ supportActionBar?.setDisplayHomeAsUpEnabled(true)
+ }
+
+ public override fun onResume() {
+ super.onResume()
+ mScannerView!!.setResultHandler(this) // Register ourselves as a handler for scan results.
+ mScannerView!!.startCamera() // Start camera on resume
+ }
+
+ public override fun onPause() {
+ super.onPause()
+ mScannerView!!.stopCamera() // Stop camera on pause
+ }
+
+ override fun handleResult(rawResult: Result) {
+ // Do something with the result here
+// Log.v(FragmentActivity.TAG, rawResult.text) // Prints scan results
+// Log.v(FragmentActivity.TAG, rawResult.barcodeFormat.toString()) // Prints the scan format (qrcode, pdf417 etc.)
+
+ finished(rawResult.text)
+
+ // If you would like to resume scanning, call this method below:
+// mScannerView!!.resumeCameraPreview(this)
+ }
+
+ private fun finished(text: String) {
+ val intent = Intent()
+ intent.putExtra("SCAN_RESULT", text)
+ setResult(Activity.RESULT_OK, intent)
+ finish()
+ }
+
+ override fun onCreateOptionsMenu(menu: Menu): Boolean {
+ menuInflater.inflate(R.menu.menu_scanner, menu)
+ return true
+ }
+
+ override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
+ R.id.select_photo -> {
+ RxPermissions(this)
+ .request(Manifest.permission.READ_EXTERNAL_STORAGE)
+ .subscribe {
+ if (it) {
+ try {
+ showFileChooser()
+ } catch (e: Exception) {
+ e.printStackTrace()
+ }
+ } else
+ toast(R.string.toast_permission_denied)
+ }
+ true
+ }
+ else -> super.onOptionsItemSelected(item)
+ }
+
+ private fun showFileChooser() {
+ val intent = Intent(Intent.ACTION_GET_CONTENT)
+ intent.type = "image/*"
+ intent.addCategory(Intent.CATEGORY_OPENABLE)
+ //intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)
+
+ try {
+ chooseFile.launch(Intent.createChooser(intent, getString(R.string.title_file_chooser)))
+ } catch (ex: android.content.ActivityNotFoundException) {
+ toast(R.string.toast_require_file_manager)
+ }
+ }
+
+ private val chooseFile = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
+ val uri = it.data?.data
+ if (it.resultCode == RESULT_OK && uri != null) {
+ try {
+ val bitmap = BitmapFactory.decodeStream(contentResolver.openInputStream(uri))
+ val text = QRCodeDecoder.syncDecodeQRCode(bitmap)
+ finished(text!!)
+ } catch (e: Exception) {
+ e.printStackTrace()
+ toast(e.message.toString())
+ }
+ }
+ }
+}
diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/ServerActivity.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/ServerActivity.kt
new file mode 100644
index 00000000..7fc1973b
--- /dev/null
+++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/ServerActivity.kt
@@ -0,0 +1,412 @@
+package com.v2ray.ang.ui
+
+import android.os.Bundle
+import android.text.TextUtils
+import android.view.Menu
+import android.view.MenuItem
+import android.view.View
+import android.widget.*
+import androidx.appcompat.app.AlertDialog
+import com.tencent.mmkv.MMKV
+import com.v2ray.ang.AppConfig
+import com.v2ray.ang.AppConfig.PREF_ALLOW_INSECURE
+import com.v2ray.ang.R
+import com.v2ray.ang.dto.EConfigType
+import com.v2ray.ang.dto.ServerConfig
+import com.v2ray.ang.dto.V2rayConfig
+import com.v2ray.ang.dto.V2rayConfig.Companion.DEFAULT_PORT
+import com.v2ray.ang.extension.toast
+import com.v2ray.ang.util.MmkvManager
+import com.v2ray.ang.util.MmkvManager.ID_MAIN
+import com.v2ray.ang.util.MmkvManager.KEY_SELECTED_SERVER
+import com.v2ray.ang.util.Utils
+
+class ServerActivity : BaseActivity() {
+
+ private val mainStorage by lazy { MMKV.mmkvWithID(ID_MAIN, MMKV.MULTI_PROCESS_MODE) }
+ private val settingsStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_SETTING, MMKV.MULTI_PROCESS_MODE) }
+ private val editGuid by lazy { intent.getStringExtra("guid").orEmpty() }
+ private val isRunning by lazy {
+ intent.getBooleanExtra("isRunning", false)
+ && editGuid.isNotEmpty()
+ && editGuid == mainStorage?.decodeString(KEY_SELECTED_SERVER)
+ }
+ private val createConfigType by lazy {
+ EConfigType.fromInt(intent.getIntExtra("createConfigType", EConfigType.VMESS.value)) ?: EConfigType.VMESS
+ }
+ private val subscriptionId by lazy {
+ intent.getStringExtra("subscriptionId")
+ }
+
+ private val securitys: Array by lazy {
+ resources.getStringArray(R.array.securitys)
+ }
+ private val shadowsocksSecuritys: Array by lazy {
+ resources.getStringArray(R.array.ss_securitys)
+ }
+ private val flows: Array by lazy {
+ resources.getStringArray(R.array.flows)
+ }
+ private val networks: Array by lazy {
+ resources.getStringArray(R.array.networks)
+ }
+ private val tcpTypes: Array by lazy {
+ resources.getStringArray(R.array.header_type_tcp)
+ }
+ private val kcpAndQuicTypes: Array by lazy {
+ resources.getStringArray(R.array.header_type_kcp_and_quic)
+ }
+ private val grpcModes: Array by lazy {
+ resources.getStringArray(R.array.mode_type_grpc)
+ }
+ private val streamSecuritys: Array by lazy {
+ resources.getStringArray(R.array.streamsecurityxs)
+ }
+ private val allowinsecures: Array by lazy {
+ resources.getStringArray(R.array.allowinsecures)
+ }
+ private val uTlsItems: Array by lazy {
+ resources.getStringArray(R.array.streamsecurity_utls)
+ }
+ private val alpns: Array by lazy {
+ resources.getStringArray(R.array.streamsecurity_alpn)
+ }
+ // Kotlin synthetics was used, but since it is removed in 1.8. We switch to old manual approach.
+ // We don't use AndroidViewBinding because, it is better to share similar logics for different
+ // protocols. Use findViewById manually ensures the xml are de-coupled with the activity logic.
+ private val et_remarks: EditText by lazy { findViewById(R.id.et_remarks) }
+ private val et_address: EditText by lazy { findViewById(R.id.et_address) }
+ private val et_port: EditText by lazy { findViewById(R.id.et_port) }
+ private val et_id: EditText by lazy { findViewById(R.id.et_id) }
+ private val et_alterId: EditText? by lazy { findViewById(R.id.et_alterId) }
+ private val et_security: EditText? by lazy { findViewById(R.id.et_security) }
+ private val sp_flow: Spinner? by lazy { findViewById(R.id.sp_flow) }
+ private val sp_security: Spinner? by lazy { findViewById(R.id.sp_security) }
+ private val sp_stream_security: Spinner? by lazy { findViewById(R.id.sp_stream_security) }
+ private val sp_allow_insecure: Spinner? by lazy { findViewById(R.id.sp_allow_insecure) }
+ private val et_sni: EditText? by lazy { findViewById(R.id.et_sni) }
+ private val sp_stream_fingerprint: Spinner? by lazy { findViewById(R.id.sp_stream_fingerprint) } //uTLS
+ private val sp_network: Spinner? by lazy { findViewById(R.id.sp_network) }
+ private val sp_header_type: Spinner? by lazy { findViewById(R.id.sp_header_type) }
+ private val sp_header_type_title: TextView? by lazy { findViewById(R.id.sp_header_type_title) }
+ private val et_request_host: EditText? by lazy { findViewById(R.id.et_request_host) }
+ private val et_path: EditText? by lazy { findViewById(R.id.et_path) }
+ private val sp_stream_alpn: Spinner? by lazy { findViewById(R.id.sp_stream_alpn) } //uTLS
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ title = getString(R.string.title_server)
+
+ val config = MmkvManager.decodeServerConfig(editGuid)
+ when(config?.configType ?: createConfigType) {
+ EConfigType.VMESS -> setContentView(R.layout.activity_server_vmess)
+ EConfigType.CUSTOM -> return
+ EConfigType.SHADOWSOCKS -> setContentView(R.layout.activity_server_shadowsocks)
+ EConfigType.SOCKS -> setContentView(R.layout.activity_server_socks)
+ EConfigType.VLESS -> setContentView(R.layout.activity_server_vless)
+ EConfigType.TROJAN -> setContentView(R.layout.activity_server_trojan)
+ }
+ sp_network?.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
+ override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
+ val types = transportTypes(networks[position])
+ sp_header_type?.isEnabled = types.size > 1
+ val adapter = ArrayAdapter(this@ServerActivity, android.R.layout.simple_spinner_item, types)
+ adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
+ sp_header_type?.adapter = adapter
+ sp_header_type_title?.text = if (networks[position] == "grpc")
+ getString(R.string.server_lab_mode_type) else
+ getString(R.string.server_lab_head_type)
+ config?.getProxyOutbound()?.getTransportSettingDetails()?.let { transportDetails ->
+ sp_header_type?.setSelection(Utils.arrayFind(types, transportDetails[0]))
+ et_request_host?.text = Utils.getEditable(transportDetails[1])
+ et_path?.text = Utils.getEditable(transportDetails[2])
+ }
+ }
+ override fun onNothingSelected(parent: AdapterView<*>?) {
+ // do nothing
+ }
+ }
+ if (config != null) {
+ bindingServer(config)
+ } else {
+ clearServer()
+ }
+ supportActionBar?.setDisplayHomeAsUpEnabled(true)
+ }
+
+ /**
+ * bingding seleced server config
+ */
+ private fun bindingServer(config: ServerConfig): Boolean {
+ val outbound = config.getProxyOutbound() ?: return false
+ val streamSetting = config.outboundBean?.streamSettings ?: return false
+
+ et_remarks.text = Utils.getEditable(config.remarks)
+ et_address.text = Utils.getEditable(outbound.getServerAddress().orEmpty())
+ et_port.text = Utils.getEditable(outbound.getServerPort()?.toString() ?: DEFAULT_PORT.toString())
+ et_id.text = Utils.getEditable(outbound.getPassword().orEmpty())
+ et_alterId?.text = Utils.getEditable(outbound.settings?.vnext?.get(0)?.users?.get(0)?.alterId.toString())
+ if (config.configType == EConfigType.SOCKS) {
+ et_security?.text = Utils.getEditable(outbound.settings?.servers?.get(0)?.users?.get(0)?.user.orEmpty())
+ } else if (config.configType == EConfigType.VLESS) {
+ et_security?.text = Utils.getEditable(outbound.getSecurityEncryption().orEmpty())
+ val flow = Utils.arrayFind(flows, outbound.settings?.vnext?.get(0)?.users?.get(0)?.flow.orEmpty())
+ if (flow >= 0) {
+ sp_flow?.setSelection(flow)
+ }
+ } else if (config.configType == EConfigType.TROJAN) {
+ val flow = Utils.arrayFind(flows, outbound.settings?.servers?.get(0)?.flow.orEmpty())
+ if (flow >= 0) {
+ sp_flow?.setSelection(flow)
+ }
+ }
+ val securityEncryptions = if (config.configType == EConfigType.SHADOWSOCKS) shadowsocksSecuritys else securitys
+ val security = Utils.arrayFind(securityEncryptions, outbound.getSecurityEncryption().orEmpty())
+ if (security >= 0) {
+ sp_security?.setSelection(security)
+ }
+
+ val streamSecurity = Utils.arrayFind(streamSecuritys, streamSetting.security)
+ if (streamSecurity >= 0) {
+ sp_stream_security?.setSelection(streamSecurity)
+ (streamSetting.tlsSettings?: streamSetting.xtlsSettings)?.let { tlsSetting ->
+ val allowinsecure = Utils.arrayFind(allowinsecures, tlsSetting.allowInsecure.toString())
+ if (allowinsecure >= 0) {
+ sp_allow_insecure?.setSelection(allowinsecure)
+ }
+ et_sni?.text = Utils.getEditable(tlsSetting.serverName)
+
+ tlsSetting.fingerprint?.let {
+ val utlsIndex = Utils.arrayFind(uTlsItems, tlsSetting.fingerprint)
+ sp_stream_fingerprint?.setSelection(utlsIndex)
+ }
+ tlsSetting.alpn?.let {
+ val alpnIndex = Utils.arrayFind(alpns, Utils.removeWhiteSpace(tlsSetting.alpn.joinToString())!!)
+ sp_stream_alpn?.setSelection(alpnIndex)
+ }
+
+ }
+ }
+ val network = Utils.arrayFind(networks, streamSetting.network)
+ if (network >= 0) {
+ sp_network?.setSelection(network)
+ }
+ return true
+ }
+
+ /**
+ * clear or init server config
+ */
+ private fun clearServer(): Boolean {
+ et_remarks.text = null
+ et_address.text = null
+ et_port.text = Utils.getEditable(DEFAULT_PORT.toString())
+ et_id.text = null
+ et_alterId?.text = Utils.getEditable("0")
+ sp_security?.setSelection(0)
+ sp_network?.setSelection(0)
+
+ sp_header_type?.setSelection(0)
+ et_request_host?.text = null
+ et_path?.text = null
+ sp_stream_security?.setSelection(0)
+ sp_allow_insecure?.setSelection(0)
+ et_sni?.text = null
+
+ //et_security.text = null
+ sp_flow?.setSelection(0)
+ return true
+ }
+
+ /**
+ * save server config
+ */
+ private fun saveServer(): Boolean {
+ if (TextUtils.isEmpty(et_remarks.text.toString())) {
+ toast(R.string.server_lab_remarks)
+ return false
+ }
+ if (TextUtils.isEmpty(et_address.text.toString())) {
+ toast(R.string.server_lab_address)
+ return false
+ }
+ val port = Utils.parseInt(et_port.text.toString())
+ if (port <= 0) {
+ toast(R.string.server_lab_port)
+ return false
+ }
+ val config = MmkvManager.decodeServerConfig(editGuid) ?: ServerConfig.create(createConfigType)
+ if (config.configType != EConfigType.SOCKS && TextUtils.isEmpty(et_id.text.toString())) {
+ toast(R.string.server_lab_id)
+ return false
+ }
+ sp_stream_security?.let {
+ if (config.configType == EConfigType.TROJAN && TextUtils.isEmpty(streamSecuritys[it.selectedItemPosition])) {
+ toast(R.string.server_lab_stream_security)
+ return false
+ }
+ }
+ et_alterId?.let {
+ val alterId = Utils.parseInt(it.text.toString())
+ if (alterId < 0) {
+ toast(R.string.server_lab_alterid)
+ return false
+ }
+ }
+
+ config.remarks = et_remarks.text.toString().trim()
+ config.outboundBean?.settings?.vnext?.get(0)?.let { vnext ->
+ saveVnext(vnext, port, config)
+ }
+ config.outboundBean?.settings?.servers?.get(0)?.let { server ->
+ saveServers(server, port, config)
+ }
+ config.outboundBean?.streamSettings?.let {
+ saveStreamSettings(it)
+ }
+ if(config.subscriptionId.isEmpty() && !subscriptionId.isNullOrEmpty()) {
+ config.subscriptionId = subscriptionId!!
+ }
+
+ MmkvManager.encodeServerConfig(editGuid, config)
+ toast(R.string.toast_success)
+ finish()
+ return true
+ }
+
+ private fun saveVnext(vnext: V2rayConfig.OutboundBean.OutSettingsBean.VnextBean, port: Int, config: ServerConfig) {
+ vnext.address = et_address.text.toString().trim()
+ vnext.port = port
+ vnext.users[0].id = et_id.text.toString().trim()
+ if (config.configType == EConfigType.VMESS) {
+ vnext.users[0].alterId = Utils.parseInt(et_alterId?.text.toString())
+ vnext.users[0].security = securitys[sp_security?.selectedItemPosition ?: 0]
+ } else if (config.configType == EConfigType.VLESS) {
+ vnext.users[0].encryption = et_security?.text.toString().trim()
+ vnext.users[0].flow = flows[sp_flow?.selectedItemPosition ?: 0]
+ }
+ }
+
+ private fun saveServers(server: V2rayConfig.OutboundBean.OutSettingsBean.ServersBean, port: Int, config: ServerConfig) {
+ server.address = et_address.text.toString().trim()
+ server.port = port
+ if (config.configType == EConfigType.SHADOWSOCKS) {
+ server.password = et_id.text.toString().trim()
+ server.method = shadowsocksSecuritys[sp_security?.selectedItemPosition ?: 0]
+ } else if (config.configType == EConfigType.SOCKS) {
+ if (TextUtils.isEmpty(et_security?.text) && TextUtils.isEmpty(et_id.text)) {
+ server.users = null
+ } else {
+ val socksUsersBean = V2rayConfig.OutboundBean.OutSettingsBean.ServersBean.SocksUsersBean()
+ socksUsersBean.user = et_security?.text.toString().trim()
+ socksUsersBean.pass = et_id.text.toString().trim()
+ server.users = listOf(socksUsersBean)
+ }
+ } else if (config.configType == EConfigType.TROJAN) {
+ server.password = et_id.text.toString().trim()
+ server.flow =
+ if (streamSecuritys[sp_stream_security?.selectedItemPosition ?: 0] == V2rayConfig.XTLS) {
+ flows[sp_flow?.selectedItemPosition ?: 0]
+ } else {
+ ""
+ }
+ }
+ }
+
+ private fun saveStreamSettings(streamSetting: V2rayConfig.OutboundBean.StreamSettingsBean) {
+ val network = sp_network?.selectedItemPosition ?: return
+ val type = sp_header_type?.selectedItemPosition ?: return
+ val requestHost = et_request_host?.text?.toString()?.trim() ?: return
+ val path = et_path?.text?.toString()?.trim() ?: return
+ val sniField = et_sni?.text?.toString()?.trim() ?: return
+ val allowInsecureField = sp_allow_insecure?.selectedItemPosition ?: return
+ val streamSecurity = sp_stream_security?.selectedItemPosition ?: return
+ var utlsIndex = sp_stream_fingerprint?.selectedItemPosition ?: return
+ var alpnIndex = sp_stream_alpn?.selectedItemPosition ?: return
+
+ var sni = streamSetting.populateTransportSettings(
+ transport = networks[network],
+ headerType = transportTypes(networks[network])[type],
+ host = requestHost,
+ path = path,
+ seed = path,
+ quicSecurity = requestHost,
+ key = path,
+ mode = transportTypes(networks[network])[type],
+ serviceName = path
+ )
+ if (sniField.isNotBlank()) {
+ sni = sniField
+ }
+ val allowInsecure = if (allowinsecures[allowInsecureField].isBlank()) {
+ settingsStorage?.decodeBool(PREF_ALLOW_INSECURE) ?: false
+ } else {
+ allowinsecures[allowInsecureField].toBoolean()
+ }
+
+ streamSetting.populateTlsSettings(streamSecuritys[streamSecurity], allowInsecure, sni, uTlsItems[utlsIndex], alpns[alpnIndex])
+ }
+
+ private fun transportTypes(network: String?): Array {
+ return if (network == "tcp") {
+ tcpTypes
+ } else if (network == "kcp" || network == "quic") {
+ kcpAndQuicTypes
+ } else if (network == "grpc") {
+ grpcModes
+ } else {
+ arrayOf("---")
+ }
+ }
+
+ /**
+ * save server config
+ */
+ private fun deleteServer(): Boolean {
+ if (editGuid.isNotEmpty()) {
+ if (editGuid != mainStorage?.decodeString(MmkvManager.KEY_SELECTED_SERVER)) {
+ if (settingsStorage?.decodeBool(AppConfig.PREF_CONFIRM_REMOVE) == true) {
+ AlertDialog.Builder(this).setMessage(R.string.del_config_comfirm)
+ .setPositiveButton(android.R.string.ok) { _, _ ->
+ MmkvManager.removeServer(editGuid)
+ finish()
+ }
+ .show()
+ } else {
+ MmkvManager.removeServer(editGuid)
+ finish()
+ }
+ }
+ }
+ return true
+ }
+
+ override fun onCreateOptionsMenu(menu: Menu): Boolean {
+ menuInflater.inflate(R.menu.action_server, menu)
+ val delButton = menu.findItem(R.id.del_config)
+ val saveButton = menu.findItem(R.id.save_config)
+
+ if (editGuid.isNotEmpty()) {
+ if (isRunning) {
+ delButton?.isVisible = false
+ saveButton?.isVisible = false
+ }
+ } else {
+ delButton?.isVisible = false
+ }
+
+ return super.onCreateOptionsMenu(menu)
+ }
+
+ override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
+ R.id.del_config -> {
+ deleteServer()
+ true
+ }
+ R.id.save_config -> {
+ saveServer()
+ true
+ }
+ else -> super.onOptionsItemSelected(item)
+ }
+}
diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/ServerCustomConfigActivity.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/ServerCustomConfigActivity.kt
new file mode 100644
index 00000000..c8138915
--- /dev/null
+++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/ServerCustomConfigActivity.kt
@@ -0,0 +1,144 @@
+package com.v2ray.ang.ui
+
+import android.os.Bundle
+import android.text.TextUtils
+import android.view.Menu
+import android.view.MenuItem
+import android.widget.Toast
+import androidx.appcompat.app.AlertDialog
+import com.blacksquircle.ui.language.json.JsonLanguage
+import com.google.gson.*
+import com.tencent.mmkv.MMKV
+import com.v2ray.ang.R
+import com.v2ray.ang.databinding.ActivityServerCustomConfigBinding
+import com.v2ray.ang.dto.EConfigType
+import com.v2ray.ang.dto.ServerConfig
+import com.v2ray.ang.dto.V2rayConfig
+import com.v2ray.ang.extension.toast
+import com.v2ray.ang.util.MmkvManager
+import com.v2ray.ang.util.Utils
+import me.drakeet.support.toast.ToastCompat
+
+class ServerCustomConfigActivity : BaseActivity() {
+ private lateinit var binding: ActivityServerCustomConfigBinding
+
+ private val mainStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_MAIN, MMKV.MULTI_PROCESS_MODE) }
+ private val serverRawStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_SERVER_RAW, MMKV.MULTI_PROCESS_MODE) }
+ private val editGuid by lazy { intent.getStringExtra("guid").orEmpty() }
+ private val isRunning by lazy {
+ intent.getBooleanExtra("isRunning", false)
+ && editGuid.isNotEmpty()
+ && editGuid == mainStorage?.decodeString(MmkvManager.KEY_SELECTED_SERVER)
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ binding = ActivityServerCustomConfigBinding.inflate(layoutInflater)
+ val view = binding.root
+ setContentView(view)
+ title = getString(R.string.title_server)
+
+ binding.editor.language = JsonLanguage()
+ val config = MmkvManager.decodeServerConfig(editGuid)
+ if (config != null) {
+ bindingServer(config)
+ } else {
+ clearServer()
+ }
+ supportActionBar?.setDisplayHomeAsUpEnabled(true)
+ }
+
+ /**
+ * bingding seleced server config
+ */
+ private fun bindingServer(config: ServerConfig): Boolean {
+ binding.etRemarks.text = Utils.getEditable(config.remarks)
+ val raw = serverRawStorage?.decodeString(editGuid)
+ if (raw.isNullOrBlank()) {
+ binding.editor.setTextContent(Utils.getEditable(config.fullConfig?.toPrettyPrinting().orEmpty()))
+ } else {
+ binding.editor.setTextContent(Utils.getEditable(raw))
+ }
+ return true
+ }
+
+ /**
+ * clear or init server config
+ */
+ private fun clearServer(): Boolean {
+ binding.etRemarks.text = null
+ return true
+ }
+
+ /**
+ * save server config
+ */
+ private fun saveServer(): Boolean {
+ if (TextUtils.isEmpty(binding.etRemarks.text.toString())) {
+ toast(R.string.server_lab_remarks)
+ return false
+ }
+
+ val v2rayConfig = try {
+ Gson().fromJson(binding.editor.text.toString(), V2rayConfig::class.java)
+ } catch (e: Exception) {
+ e.printStackTrace()
+ ToastCompat.makeText(this, "${getString(R.string.toast_malformed_josn)} ${e.cause?.message}", Toast.LENGTH_LONG).show()
+ return false
+ }
+
+ val config = MmkvManager.decodeServerConfig(editGuid) ?: ServerConfig.create(EConfigType.CUSTOM)
+ config.remarks = binding.etRemarks.text.toString().trim()
+ config.fullConfig = v2rayConfig
+
+ MmkvManager.encodeServerConfig(editGuid, config)
+ serverRawStorage?.encode(editGuid, binding.editor.text.toString())
+ toast(R.string.toast_success)
+ finish()
+ return true
+ }
+
+ /**
+ * save server config
+ */
+ private fun deleteServer(): Boolean {
+ if (editGuid.isNotEmpty()) {
+ AlertDialog.Builder(this).setMessage(R.string.del_config_comfirm)
+ .setPositiveButton(android.R.string.ok) { _, _ ->
+ MmkvManager.removeServer(editGuid)
+ finish()
+ }
+ .show()
+ }
+ return true
+ }
+
+ override fun onCreateOptionsMenu(menu: Menu): Boolean {
+ menuInflater.inflate(R.menu.action_server, menu)
+ val delButton = menu.findItem(R.id.del_config)
+ val saveButton = menu.findItem(R.id.save_config)
+
+ if (editGuid.isNotEmpty()) {
+ if (isRunning) {
+ delButton?.isVisible = false
+ saveButton?.isVisible = false
+ }
+ } else {
+ delButton?.isVisible = false
+ }
+
+ return super.onCreateOptionsMenu(menu)
+ }
+
+ override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
+ R.id.del_config -> {
+ deleteServer()
+ true
+ }
+ R.id.save_config -> {
+ saveServer()
+ true
+ }
+ else -> super.onOptionsItemSelected(item)
+ }
+}
diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/SettingsActivity.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/SettingsActivity.kt
new file mode 100644
index 00000000..5284cc64
--- /dev/null
+++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/SettingsActivity.kt
@@ -0,0 +1,182 @@
+package com.v2ray.ang.ui
+
+import android.content.Intent
+import android.os.Bundle
+import android.text.TextUtils
+import android.view.View
+import androidx.activity.viewModels
+import androidx.preference.*
+import com.v2ray.ang.AppConfig
+import com.v2ray.ang.R
+import com.v2ray.ang.util.Utils
+import com.v2ray.ang.viewmodel.SettingsViewModel
+
+class SettingsActivity : BaseActivity() {
+ private val settingsViewModel: SettingsViewModel by viewModels()
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_settings)
+
+ title = getString(R.string.title_settings)
+
+ supportActionBar?.setDisplayHomeAsUpEnabled(true)
+
+ settingsViewModel.startListenPreferenceChange()
+ }
+
+ class SettingsFragment : PreferenceFragmentCompat() {
+ private val perAppProxy by lazy { findPreference(AppConfig.PREF_PER_APP_PROXY) }
+ private val localDns by lazy { findPreference(AppConfig.PREF_LOCAL_DNS_ENABLED) }
+ private val fakeDns by lazy { findPreference(AppConfig.PREF_FAKE_DNS_ENABLED) }
+ private val localDnsPort by lazy { findPreference(AppConfig.PREF_LOCAL_DNS_PORT) }
+ private val vpnDns by lazy { findPreference(AppConfig.PREF_VPN_DNS) }
+ // val autoRestart by lazy { findPreference(PREF_AUTO_RESTART) as CheckBoxPreference }
+ private val remoteDns by lazy { findPreference(AppConfig.PREF_REMOTE_DNS) }
+ private val domesticDns by lazy { findPreference(AppConfig.PREF_DOMESTIC_DNS) }
+ private val socksPort by lazy { findPreference(AppConfig.PREF_SOCKS_PORT) }
+ private val httpPort by lazy { findPreference(AppConfig.PREF_HTTP_PORT) }
+ private val routingCustom by lazy { findPreference(AppConfig.PREF_ROUTING_CUSTOM) }
+ // val licenses: Preference by lazy { findPreference(PREF_LICENSES) }
+// val feedback: Preference by lazy { findPreference(PREF_FEEDBACK) }
+// val tgGroup: Preference by lazy { findPreference(PREF_TG_GROUP) }
+
+ private val mode by lazy { findPreference(AppConfig.PREF_MODE) }
+
+ override fun onCreatePreferences(bundle: Bundle?, s: String?) {
+ addPreferencesFromResource(R.xml.pref_settings)
+
+ routingCustom?.setOnPreferenceClickListener {
+ startActivity(Intent(activity, RoutingSettingsActivity::class.java))
+ false
+ }
+
+// licenses.onClick {
+// val fragment = LicensesDialogFragment.Builder(act)
+// .setNotices(R.raw.licenses)
+// .setIncludeOwnLicense(false)
+// .build()
+// fragment.show((act as AppCompatActivity).supportFragmentManager, null)
+// }
+//
+// feedback.onClick {
+// Utils.openUri(activity, "https://github.com/2dust/v2rayNG/issues")
+// }
+// tgGroup.onClick {
+// // Utils.openUri(activity, "https://t.me/v2rayN")
+// val intent = Intent(Intent.ACTION_VIEW, Uri.parse("tg:resolve?domain=v2rayN"))
+// try {
+// startActivity(intent)
+// } catch (e: Exception) {
+// e.printStackTrace()
+// toast(R.string.toast_tg_app_not_found)
+// }
+// }
+
+ perAppProxy?.setOnPreferenceClickListener {
+ startActivity(Intent(activity, PerAppProxyActivity::class.java))
+ perAppProxy?.isChecked = true
+ false
+ }
+
+ remoteDns?.setOnPreferenceChangeListener { _, any ->
+ // remoteDns.summary = any as String
+ val nval = any as String
+ remoteDns?.summary = if (nval == "") AppConfig.DNS_AGENT else nval
+ true
+ }
+ domesticDns?.setOnPreferenceChangeListener { _, any ->
+ // domesticDns.summary = any as String
+ val nval = any as String
+ domesticDns?.summary = if (nval == "") AppConfig.DNS_DIRECT else nval
+ true
+ }
+
+ localDns?.setOnPreferenceChangeListener{ _, any ->
+ updateLocalDns(any as Boolean)
+ true
+ }
+ localDnsPort?.setOnPreferenceChangeListener { _, any ->
+ val nval = any as String
+ localDnsPort?.summary = if (TextUtils.isEmpty(nval)) AppConfig.PORT_LOCAL_DNS else nval
+ true
+ }
+ vpnDns?.setOnPreferenceChangeListener { _, any ->
+ vpnDns?.summary = any as String
+ true
+ }
+ socksPort?.setOnPreferenceChangeListener { _, any ->
+ val nval = any as String
+ socksPort?.summary = if (TextUtils.isEmpty(nval)) AppConfig.PORT_SOCKS else nval
+ true
+ }
+ httpPort?.setOnPreferenceChangeListener { _, any ->
+ val nval = any as String
+ httpPort?.summary = if (TextUtils.isEmpty(nval)) AppConfig.PORT_HTTP else nval
+ true
+ }
+ mode?.setOnPreferenceChangeListener { _, newValue ->
+ updateMode(newValue.toString())
+ true
+ }
+ mode?.dialogLayoutResource = R.layout.preference_with_help_link
+ //loglevel.summary = "LogLevel"
+ }
+
+ override fun onStart() {
+ super.onStart()
+ val defaultSharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireActivity())
+ updateMode(defaultSharedPreferences.getString(AppConfig.PREF_MODE, "VPN"))
+ var remoteDnsString = defaultSharedPreferences.getString(AppConfig.PREF_REMOTE_DNS, "")
+ domesticDns?.summary = defaultSharedPreferences.getString(AppConfig.PREF_DOMESTIC_DNS, "")
+
+ localDnsPort?.summary = defaultSharedPreferences.getString(AppConfig.PREF_LOCAL_DNS_PORT, AppConfig.PORT_LOCAL_DNS)
+ socksPort?.summary = defaultSharedPreferences.getString(AppConfig.PREF_SOCKS_PORT, AppConfig.PORT_SOCKS)
+ httpPort?.summary = defaultSharedPreferences.getString(AppConfig.PREF_HTTP_PORT, AppConfig.PORT_HTTP)
+
+ if (TextUtils.isEmpty(remoteDnsString)) {
+ remoteDnsString = AppConfig.DNS_AGENT
+ }
+ if (TextUtils.isEmpty(domesticDns?.summary)) {
+ domesticDns?.summary = AppConfig.DNS_DIRECT
+ }
+ remoteDns?.summary = remoteDnsString
+ vpnDns?.summary = defaultSharedPreferences.getString(AppConfig.PREF_VPN_DNS, remoteDnsString)
+
+ if (TextUtils.isEmpty(localDnsPort?.summary)) {
+ localDnsPort?.summary = AppConfig.PORT_LOCAL_DNS
+ }
+ if (TextUtils.isEmpty(socksPort?.summary)) {
+ socksPort?.summary = AppConfig.PORT_SOCKS
+ }
+ if (TextUtils.isEmpty(httpPort?.summary)) {
+ httpPort?.summary = AppConfig.PORT_HTTP
+ }
+ }
+
+ private fun updateMode(mode: String?) {
+ val defaultSharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireActivity())
+ val vpn = mode == "VPN"
+ perAppProxy?.isEnabled = vpn
+ perAppProxy?.isChecked = PreferenceManager.getDefaultSharedPreferences(requireActivity())
+ .getBoolean(AppConfig.PREF_PER_APP_PROXY, false)
+ localDns?.isEnabled = vpn
+ fakeDns?.isEnabled = vpn
+ localDnsPort?.isEnabled = vpn
+ vpnDns?.isEnabled = vpn
+ if (vpn) {
+ updateLocalDns(defaultSharedPreferences.getBoolean(AppConfig.PREF_LOCAL_DNS_ENABLED, false))
+ }
+ }
+
+ private fun updateLocalDns(enabled: Boolean) {
+ fakeDns?.isEnabled = enabled
+ localDnsPort?.isEnabled = enabled
+ vpnDns?.isEnabled = !enabled
+ }
+ }
+
+ fun onModeHelpClicked(view: View) {
+ Utils.openUri(this, AppConfig.v2rayNGWikiMode)
+ }
+}
diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/SubEditActivity.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/SubEditActivity.kt
new file mode 100644
index 00000000..5a5a8d44
--- /dev/null
+++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/SubEditActivity.kt
@@ -0,0 +1,133 @@
+package com.v2ray.ang.ui
+
+import android.os.Bundle
+import android.text.TextUtils
+import android.view.Menu
+import android.view.MenuItem
+import androidx.appcompat.app.AlertDialog
+import com.google.gson.Gson
+import com.tencent.mmkv.MMKV
+import com.v2ray.ang.R
+import com.v2ray.ang.databinding.ActivitySubEditBinding
+import com.v2ray.ang.dto.SubscriptionItem
+import com.v2ray.ang.extension.toast
+import com.v2ray.ang.util.MmkvManager
+import com.v2ray.ang.util.Utils
+
+class SubEditActivity : BaseActivity() {
+ private lateinit var binding: ActivitySubEditBinding
+
+ var del_config: MenuItem? = null
+ var save_config: MenuItem? = null
+
+ private val subStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_SUB, MMKV.MULTI_PROCESS_MODE) }
+ private val editSubId by lazy { intent.getStringExtra("subId").orEmpty() }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ binding = ActivitySubEditBinding.inflate(layoutInflater)
+ val view = binding.root
+ setContentView(view)
+ title = getString(R.string.title_sub_setting)
+
+ val json = subStorage?.decodeString(editSubId)
+ if (!json.isNullOrBlank()) {
+ bindingServer(Gson().fromJson(json, SubscriptionItem::class.java))
+ } else {
+ clearServer()
+ }
+ supportActionBar?.setDisplayHomeAsUpEnabled(true)
+ }
+
+ /**
+ * bingding seleced server config
+ */
+ private fun bindingServer(subItem: SubscriptionItem): Boolean {
+ binding.etRemarks.text = Utils.getEditable(subItem.remarks)
+ binding.etUrl.text = Utils.getEditable(subItem.url)
+ binding.chkEnable.isChecked = subItem.enabled
+ return true
+ }
+
+ /**
+ * clear or init server config
+ */
+ private fun clearServer(): Boolean {
+ binding.etRemarks.text = null
+ binding.etUrl.text = null
+ binding.chkEnable.isChecked = true
+ return true
+ }
+
+ /**
+ * save server config
+ */
+ private fun saveServer(): Boolean {
+ val subItem: SubscriptionItem
+ val json = subStorage?.decodeString(editSubId)
+ var subId = editSubId
+ if (!json.isNullOrBlank()) {
+ subItem = Gson().fromJson(json, SubscriptionItem::class.java)
+ } else {
+ subId = Utils.getUuid()
+ subItem = SubscriptionItem()
+ }
+
+ subItem.remarks = binding.etRemarks.text.toString()
+ subItem.url = binding.etUrl.text.toString()
+ subItem.enabled = binding.chkEnable.isChecked
+
+ if (TextUtils.isEmpty(subItem.remarks)) {
+ toast(R.string.sub_setting_remarks)
+ return false
+ }
+// if (TextUtils.isEmpty(subItem.url)) {
+// toast(R.string.sub_setting_url)
+// return false
+// }
+
+ subStorage?.encode(subId, Gson().toJson(subItem))
+ toast(R.string.toast_success)
+ finish()
+ return true
+ }
+
+ /**
+ * save server config
+ */
+ private fun deleteServer(): Boolean {
+ if (editSubId.isNotEmpty()) {
+ AlertDialog.Builder(this).setMessage(R.string.del_config_comfirm)
+ .setPositiveButton(android.R.string.ok) { _, _ ->
+ MmkvManager.removeSubscription(editSubId)
+ finish()
+ }
+ .show()
+ }
+ return true
+ }
+
+ override fun onCreateOptionsMenu(menu: Menu): Boolean {
+ menuInflater.inflate(R.menu.action_server, menu)
+ del_config = menu.findItem(R.id.del_config)
+ save_config = menu.findItem(R.id.save_config)
+
+ if (editSubId.isEmpty()) {
+ del_config?.isVisible = false
+ }
+
+ return super.onCreateOptionsMenu(menu)
+ }
+
+ override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
+ R.id.del_config -> {
+ deleteServer()
+ true
+ }
+ R.id.save_config -> {
+ saveServer()
+ true
+ }
+ else -> super.onOptionsItemSelected(item)
+ }
+}
diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/SubSettingActivity.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/SubSettingActivity.kt
new file mode 100644
index 00000000..853a3878
--- /dev/null
+++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/SubSettingActivity.kt
@@ -0,0 +1,55 @@
+package com.v2ray.ang.ui
+
+import android.content.Intent
+import androidx.recyclerview.widget.LinearLayoutManager
+import android.view.Menu
+import android.view.MenuItem
+import com.v2ray.ang.R
+import android.os.Bundle
+import com.v2ray.ang.databinding.ActivitySubSettingBinding
+import com.v2ray.ang.dto.SubscriptionItem
+import com.v2ray.ang.util.MmkvManager
+
+class SubSettingActivity : BaseActivity() {
+ private lateinit var binding: ActivitySubSettingBinding
+
+ var subscriptions:List> = listOf()
+ private val adapter by lazy { SubSettingRecyclerAdapter(this) }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ binding = ActivitySubSettingBinding.inflate(layoutInflater)
+ val view = binding.root
+ setContentView(view)
+
+ title = getString(R.string.title_sub_setting)
+
+ binding.recyclerView.setHasFixedSize(true)
+ binding.recyclerView.layoutManager = LinearLayoutManager(this)
+ binding.recyclerView.adapter = adapter
+
+ supportActionBar?.setDisplayHomeAsUpEnabled(true)
+ }
+
+ override fun onResume() {
+ super.onResume()
+ subscriptions = MmkvManager.decodeSubscriptions()
+ adapter.notifyDataSetChanged()
+ }
+
+ override fun onCreateOptionsMenu(menu: Menu): Boolean {
+ menuInflater.inflate(R.menu.action_sub_setting, menu)
+ menu.findItem(R.id.del_config)?.isVisible = false
+ menu.findItem(R.id.save_config)?.isVisible = false
+
+ return super.onCreateOptionsMenu(menu)
+ }
+
+ override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
+ R.id.add_config -> {
+ startActivity(Intent(this, SubEditActivity::class.java))
+ true
+ }
+ else -> super.onOptionsItemSelected(item)
+ }
+}
diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/SubSettingRecyclerAdapter.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/SubSettingRecyclerAdapter.kt
new file mode 100644
index 00000000..f3732bf3
--- /dev/null
+++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/SubSettingRecyclerAdapter.kt
@@ -0,0 +1,50 @@
+package com.v2ray.ang.ui
+
+import android.content.Intent
+import android.graphics.Color
+import android.view.LayoutInflater
+import androidx.recyclerview.widget.RecyclerView
+import android.view.ViewGroup
+import com.google.gson.Gson
+import com.tencent.mmkv.MMKV
+import com.v2ray.ang.R
+import com.v2ray.ang.databinding.ItemRecyclerSubSettingBinding
+import com.v2ray.ang.util.MmkvManager
+
+class SubSettingRecyclerAdapter(val activity: SubSettingActivity) : RecyclerView.Adapter() {
+
+ private var mActivity: SubSettingActivity = activity
+ private val subStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_SUB, MMKV.MULTI_PROCESS_MODE) }
+
+ override fun getItemCount() = mActivity.subscriptions.size
+
+ override fun onBindViewHolder(holder: MainViewHolder, position: Int) {
+ val subId = mActivity.subscriptions[position].first
+ val subItem = mActivity.subscriptions[position].second
+ holder.itemSubSettingBinding.tvName.text = subItem.remarks
+ holder.itemSubSettingBinding.tvUrl.text = subItem.url
+ if (subItem.enabled) {
+ holder.itemSubSettingBinding.chkEnable.setBackgroundResource(R.color.colorSelected)
+ } else {
+ holder.itemSubSettingBinding.chkEnable.setBackgroundResource(R.color.colorUnselected)
+ }
+ holder.itemView.setBackgroundColor(Color.TRANSPARENT)
+
+ holder.itemSubSettingBinding.layoutEdit.setOnClickListener {
+ mActivity.startActivity(Intent(mActivity, SubEditActivity::class.java)
+ .putExtra("subId", subId)
+ )
+ }
+ holder.itemSubSettingBinding.infoContainer.setOnClickListener {
+ subItem.enabled = !subItem.enabled
+ subStorage?.encode(subId, Gson().toJson(subItem))
+ notifyItemChanged(position)
+ }
+ }
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MainViewHolder {
+ return MainViewHolder(ItemRecyclerSubSettingBinding.inflate(LayoutInflater.from(parent.context), parent, false))
+ }
+
+ class MainViewHolder(val itemSubSettingBinding: ItemRecyclerSubSettingBinding) : RecyclerView.ViewHolder(itemSubSettingBinding.root)
+}
diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/TaskerActivity.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/TaskerActivity.kt
new file mode 100644
index 00000000..da7ca188
--- /dev/null
+++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/TaskerActivity.kt
@@ -0,0 +1,117 @@
+package com.v2ray.ang.ui
+
+import android.app.Activity
+import android.os.Bundle
+import android.view.View
+import android.widget.ArrayAdapter
+import android.widget.ListView
+import java.util.ArrayList
+import com.v2ray.ang.R
+import android.content.Intent
+import android.text.TextUtils
+import android.view.Menu
+import android.view.MenuItem
+import com.google.zxing.WriterException
+import com.tencent.mmkv.MMKV
+import com.v2ray.ang.AppConfig
+import com.v2ray.ang.databinding.ActivityTaskerBinding
+import com.v2ray.ang.util.MmkvManager
+
+class TaskerActivity : BaseActivity() {
+ private lateinit var binding: ActivityTaskerBinding
+
+ private var listview: ListView? = null
+ private var lstData: ArrayList = ArrayList()
+ private var lstGuid: ArrayList = ArrayList()
+
+ private val serverStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_SERVER_CONFIG, MMKV.MULTI_PROCESS_MODE) }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ binding = ActivityTaskerBinding.inflate(layoutInflater)
+ val view = binding.root
+ setContentView(view)
+
+ //add def value
+ lstData.add("Default")
+ lstGuid.add(AppConfig.TASKER_DEFAULT_GUID)
+
+ serverStorage?.allKeys()?.forEach { key ->
+ MmkvManager.decodeServerConfig(key)?.let { config ->
+ lstData.add(config.remarks)
+ lstGuid.add(key)
+ }
+ }
+ val adapter = ArrayAdapter(this,
+ android.R.layout.simple_list_item_single_choice, lstData)
+ listview = findViewById(R.id.listview) as ListView
+ listview!!.adapter = adapter
+
+ init()
+ }
+
+ private fun init() {
+ try {
+ val bundle = intent?.getBundleExtra(AppConfig.TASKER_EXTRA_BUNDLE)
+ val switch = bundle?.getBoolean(AppConfig.TASKER_EXTRA_BUNDLE_SWITCH, false)
+ val guid = bundle?.getString(AppConfig.TASKER_EXTRA_BUNDLE_GUID, "")
+
+ if (switch == null || TextUtils.isEmpty(guid)) {
+ return
+ } else {
+ binding.switchStartService.isChecked = switch
+ val pos = lstGuid.indexOf(guid.toString())
+ if (pos >= 0) {
+ listview?.setItemChecked(pos, true)
+ }
+ }
+ } catch (e: WriterException) {
+ e.printStackTrace()
+
+ }
+ }
+
+ private fun confirmFinish() {
+ val position = listview?.checkedItemPosition
+ if (position == null || position < 0) {
+ return
+ }
+
+ val extraBundle = Bundle()
+ extraBundle.putBoolean(AppConfig.TASKER_EXTRA_BUNDLE_SWITCH, binding.switchStartService.isChecked)
+ extraBundle.putString(AppConfig.TASKER_EXTRA_BUNDLE_GUID, lstGuid[position])
+ val intent = Intent()
+
+ val remarks = lstData[position]
+ val blurb = if (binding.switchStartService.isChecked) {
+ "Start $remarks"
+ } else {
+ "Stop $remarks"
+ }
+
+ intent.putExtra(AppConfig.TASKER_EXTRA_BUNDLE, extraBundle)
+ intent.putExtra(AppConfig.TASKER_EXTRA_STRING_BLURB, blurb)
+ setResult(Activity.RESULT_OK, intent)
+ finish()
+ }
+
+ override fun onCreateOptionsMenu(menu: Menu): Boolean {
+ menuInflater.inflate(R.menu.action_server, menu)
+ val del_config = menu.findItem(R.id.del_config)
+ del_config?.isVisible = false
+ return super.onCreateOptionsMenu(menu)
+ }
+
+ override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
+ R.id.del_config -> {
+ true
+ }
+ R.id.save_config -> {
+ confirmFinish()
+ true
+ }
+ else -> super.onOptionsItemSelected(item)
+ }
+
+}
+
diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/UrlSchemeActivity.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/UrlSchemeActivity.kt
new file mode 100644
index 00000000..90681100
--- /dev/null
+++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/UrlSchemeActivity.kt
@@ -0,0 +1,51 @@
+package com.v2ray.ang.ui
+
+import android.content.Intent
+import android.net.Uri
+import android.os.Bundle
+import com.google.zxing.WriterException
+import com.v2ray.ang.R
+import com.v2ray.ang.databinding.ActivityLogcatBinding
+import com.v2ray.ang.extension.toast
+import com.v2ray.ang.util.AngConfigManager
+
+class UrlSchemeActivity : BaseActivity() {
+ private lateinit var binding: ActivityLogcatBinding
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ binding = ActivityLogcatBinding.inflate(layoutInflater)
+ val view = binding.root
+ setContentView(view)
+
+ var shareUrl: String = ""
+ try {
+ intent?.apply {
+ when (action) {
+ Intent.ACTION_SEND -> {
+ if ("text/plain" == type) {
+ intent.getStringExtra(Intent.EXTRA_TEXT)?.let {
+ shareUrl = it
+ }
+ }
+ }
+ Intent.ACTION_VIEW -> {
+ val uri: Uri? = intent.data
+ shareUrl = uri?.getQueryParameter("url")!!
+ }
+ }
+ }
+ toast(shareUrl)
+ val count = AngConfigManager.importBatchConfig(shareUrl, "", false)
+ if (count > 0) {
+ toast(R.string.toast_success)
+ } else {
+ toast(R.string.toast_failure)
+ }
+ startActivity(Intent(this, MainActivity::class.java))
+ finish()
+ } catch (e: WriterException) {
+ e.printStackTrace()
+ }
+ }
+}
\ No newline at end of file
diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/UserAssetActivity.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/UserAssetActivity.kt
new file mode 100644
index 00000000..17755bfb
--- /dev/null
+++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/UserAssetActivity.kt
@@ -0,0 +1,216 @@
+package com.v2ray.ang.ui
+
+import android.Manifest
+import android.annotation.SuppressLint
+import android.content.Intent
+import android.net.Uri
+import android.os.Bundle
+import android.provider.OpenableColumns
+import android.util.Log
+import android.view.*
+import android.view.View.GONE
+import android.view.View.VISIBLE
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.lifecycle.lifecycleScope
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import com.tbruyelle.rxpermissions.RxPermissions
+import com.tencent.mmkv.MMKV
+import com.v2ray.ang.AppConfig
+import com.v2ray.ang.R
+import com.v2ray.ang.databinding.ActivitySubSettingBinding
+import com.v2ray.ang.databinding.ItemRecyclerUserAssetBinding
+import com.v2ray.ang.extension.toTrafficString
+import com.v2ray.ang.extension.toast
+import com.v2ray.ang.util.MmkvManager
+import com.v2ray.ang.util.Utils
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import java.io.File
+import java.io.FileOutputStream
+import java.net.HttpURLConnection
+import java.net.InetSocketAddress
+import java.net.Proxy
+import java.net.URL
+import java.text.DateFormat
+import java.util.*
+
+class UserAssetActivity : BaseActivity() {
+ private lateinit var binding: ActivitySubSettingBinding
+ private val settingsStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_SETTING, MMKV.MULTI_PROCESS_MODE) }
+
+ val extDir by lazy { File(Utils.userAssetPath(this)) }
+ val geofiles = arrayOf("geosite.dat", "geoip.dat")
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ binding = ActivitySubSettingBinding.inflate(layoutInflater)
+ val view = binding.root
+ setContentView(view)
+ title = getString(R.string.title_user_asset_setting)
+ supportActionBar?.setDisplayHomeAsUpEnabled(true)
+
+ binding.recyclerView.setHasFixedSize(true)
+ binding.recyclerView.layoutManager = LinearLayoutManager(this)
+ binding.recyclerView.adapter = UserAssetAdapter()
+ }
+
+ override fun onCreateOptionsMenu(menu: Menu): Boolean {
+ menuInflater.inflate(R.menu.menu_asset, menu)
+ return super.onCreateOptionsMenu(menu)
+ }
+
+ override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
+ R.id.add_file -> {
+ showFileChooser()
+ true
+ }
+
+ R.id.download_file -> {
+ downloadGeoFiles()
+ true
+ }
+
+ else -> super.onOptionsItemSelected(item)
+ }
+
+ private fun showFileChooser() {
+ RxPermissions(this).request(Manifest.permission.READ_EXTERNAL_STORAGE).subscribe {
+ if (it) {
+ val intent = Intent(Intent.ACTION_GET_CONTENT)
+ intent.type = "*/*"
+ intent.addCategory(Intent.CATEGORY_OPENABLE)
+
+ try {
+ chooseFile.launch(
+ Intent.createChooser(
+ intent,
+ getString(R.string.title_file_chooser)
+ )
+ )
+ } catch (ex: android.content.ActivityNotFoundException) {
+ toast(R.string.toast_require_file_manager)
+ }
+ }
+ }
+ }
+
+ private val chooseFile =
+ registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
+ val uri = it.data?.data
+ if (it.resultCode == RESULT_OK && uri != null) {
+ try {
+ copyFile(uri)
+ } catch (e: Exception) {
+ toast(R.string.toast_asset_copy_failed)
+ }
+ }
+ }
+
+ private fun copyFile(uri: Uri): String {
+ val targetFile = File(extDir, getCursorName(uri) ?: uri.toString())
+ contentResolver.openInputStream(uri).use { inputStream ->
+ targetFile.outputStream().use { fileOut ->
+ inputStream?.copyTo(fileOut)
+ toast(R.string.toast_success)
+ binding.recyclerView.adapter?.notifyDataSetChanged()
+ }
+ }
+ return targetFile.path
+ }
+
+ private fun getCursorName(uri: Uri): String? = try {
+ contentResolver.query(uri, null, null, null, null)?.let { cursor ->
+ cursor.run {
+ if (moveToFirst()) getString(getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME))
+ else null
+ }.also { cursor.close() }
+ }
+ } catch (e: Exception) {
+ e.printStackTrace()
+ null
+ }
+
+ private fun downloadGeoFiles() {
+ val httpPort = Utils.parseInt(settingsStorage?.decodeString(AppConfig.PREF_HTTP_PORT), AppConfig.PORT_HTTP.toInt())
+
+ toast(R.string.msg_downloading_content)
+ geofiles.forEach {
+ //toast(getString(R.string.msg_downloading_content) + it)
+ lifecycleScope.launch(Dispatchers.IO) {
+ val result = downloadGeo(it, 60000, httpPort)
+ launch(Dispatchers.Main) {
+ if (result) {
+ toast(getString(R.string.toast_success) + " " + it)
+ binding.recyclerView.adapter?.notifyDataSetChanged()
+ } else {
+ toast(getString(R.string.toast_failure) + " " + it)
+ }
+ }
+ }
+ }
+ }
+
+ private fun downloadGeo(name: String, timeout: Int, httpPort: Int): Boolean {
+ val url = AppConfig.geoUrl + name
+ val targetTemp = File(extDir, name + "_temp")
+ val target = File(extDir, name)
+ var conn: HttpURLConnection? = null
+ //Log.d(AppConfig.ANG_PACKAGE, url)
+
+ try {
+ conn = URL(url).openConnection(
+ Proxy(
+ Proxy.Type.HTTP,
+ InetSocketAddress("127.0.0.1", httpPort)
+ )
+ ) as HttpURLConnection
+ conn.connectTimeout = timeout
+ conn.readTimeout = timeout
+ val inputStream = conn.inputStream
+ val responseCode = conn.responseCode
+ if (responseCode == HttpURLConnection.HTTP_OK) {
+ FileOutputStream(targetTemp).use { output ->
+ inputStream.copyTo(output)
+ }
+
+ targetTemp.renameTo(target)
+ }
+ return true
+ } catch (e: Exception) {
+ Log.e(AppConfig.ANG_PACKAGE, Log.getStackTraceString(e))
+ return false
+ } finally {
+ conn?.disconnect()
+ }
+ }
+
+ inner class UserAssetAdapter : RecyclerView.Adapter() {
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UserAssetViewHolder {
+ return UserAssetViewHolder(ItemRecyclerUserAssetBinding.inflate(LayoutInflater.from(parent.context), parent, false))
+ }
+
+ @SuppressLint("SetTextI18n")
+ override fun onBindViewHolder(holder: UserAssetViewHolder, position: Int) {
+ val file = extDir.listFiles()?.getOrNull(position) ?: return
+ holder.itemUserAssetBinding.assetName.text = file.name
+ val dateFormat = DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.MEDIUM)
+ holder.itemUserAssetBinding.assetProperties.text = "${file.length().toTrafficString()} • ${dateFormat.format(Date(file.lastModified()))}"
+ if (file.name in geofiles) {
+ holder.itemUserAssetBinding.layoutRemove.visibility = GONE
+ } else {
+ holder.itemUserAssetBinding.layoutRemove.visibility = VISIBLE
+ }
+ holder.itemUserAssetBinding.layoutRemove.setOnClickListener {
+ file.delete()
+ binding.recyclerView.adapter?.notifyItemRemoved(position)
+ }
+ }
+
+ override fun getItemCount(): Int {
+ return extDir.listFiles()?.size ?: 0
+ }
+ }
+
+ class UserAssetViewHolder(val itemUserAssetBinding: ItemRecyclerUserAssetBinding) : RecyclerView.ViewHolder(itemUserAssetBinding.root)
+}
diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/AngConfigManager.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/AngConfigManager.kt
new file mode 100644
index 00000000..5a58a8c4
--- /dev/null
+++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/AngConfigManager.kt
@@ -0,0 +1,750 @@
+package com.v2ray.ang.util
+
+import android.content.Context
+import android.content.SharedPreferences
+import android.graphics.Bitmap
+import android.text.TextUtils
+import android.util.Log
+import androidx.preference.PreferenceManager
+import com.google.gson.Gson
+import com.tencent.mmkv.MMKV
+import com.v2ray.ang.AppConfig
+import com.v2ray.ang.AppConfig.ANG_CONFIG
+import com.v2ray.ang.AppConfig.HTTPS_PROTOCOL
+import com.v2ray.ang.AppConfig.HTTP_PROTOCOL
+import com.v2ray.ang.R
+import com.v2ray.ang.dto.*
+import com.v2ray.ang.dto.V2rayConfig.Companion.DEFAULT_SECURITY
+import com.v2ray.ang.dto.V2rayConfig.Companion.TLS
+import com.v2ray.ang.util.MmkvManager.KEY_SELECTED_SERVER
+import java.net.URI
+import java.util.*
+import com.v2ray.ang.extension.idnHost
+
+object AngConfigManager {
+ private val mainStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_MAIN, MMKV.MULTI_PROCESS_MODE) }
+ private val serverRawStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_SERVER_RAW, MMKV.MULTI_PROCESS_MODE) }
+ private val settingsStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_SETTING, MMKV.MULTI_PROCESS_MODE) }
+ private val subStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_SUB, MMKV.MULTI_PROCESS_MODE) }
+
+ /**
+ * Legacy loading config
+ */
+ fun migrateLegacyConfig(c: Context): Boolean? {
+ try {
+ val defaultSharedPreferences = PreferenceManager.getDefaultSharedPreferences(c)
+ val context = defaultSharedPreferences.getString(ANG_CONFIG, "")
+ if (context.isNullOrBlank()) {
+ return null
+ }
+ val angConfig = Gson().fromJson(context, AngConfig::class.java)
+ for (i in angConfig.vmess.indices) {
+ upgradeServerVersion(angConfig.vmess[i])
+ }
+
+ copyLegacySettings(defaultSharedPreferences)
+ migrateVmessBean(angConfig, defaultSharedPreferences)
+ migrateSubItemBean(angConfig)
+
+ defaultSharedPreferences.edit().remove(ANG_CONFIG).apply()
+ return true
+ } catch (e: Exception) {
+ e.printStackTrace()
+ }
+ return false
+ }
+
+ private fun copyLegacySettings(sharedPreferences: SharedPreferences) {
+ listOf(
+ AppConfig.PREF_MODE,
+ AppConfig.PREF_REMOTE_DNS,
+ AppConfig.PREF_DOMESTIC_DNS,
+ AppConfig.PREF_LOCAL_DNS_PORT,
+ AppConfig.PREF_SOCKS_PORT,
+ AppConfig.PREF_HTTP_PORT,
+ AppConfig.PREF_LOGLEVEL,
+ AppConfig.PREF_ROUTING_DOMAIN_STRATEGY,
+ AppConfig.PREF_ROUTING_MODE,
+ AppConfig.PREF_V2RAY_ROUTING_AGENT,
+ AppConfig.PREF_V2RAY_ROUTING_BLOCKED,
+ AppConfig.PREF_V2RAY_ROUTING_DIRECT,
+ ).forEach { key ->
+ settingsStorage?.encode(key, sharedPreferences.getString(key, null))
+ }
+ listOf(
+ AppConfig.PREF_SPEED_ENABLED,
+ AppConfig.PREF_PROXY_SHARING,
+ AppConfig.PREF_LOCAL_DNS_ENABLED,
+ AppConfig.PREF_ALLOW_INSECURE,
+ AppConfig.PREF_PREFER_IPV6,
+ AppConfig.PREF_PER_APP_PROXY,
+ AppConfig.PREF_BYPASS_APPS,
+ ).forEach { key ->
+ settingsStorage?.encode(key, sharedPreferences.getBoolean(key, false))
+ }
+ settingsStorage?.encode(AppConfig.PREF_SNIFFING_ENABLED, sharedPreferences.getBoolean(AppConfig.PREF_SNIFFING_ENABLED, true))
+ settingsStorage?.encode(AppConfig.PREF_PER_APP_PROXY_SET, sharedPreferences.getStringSet(AppConfig.PREF_PER_APP_PROXY_SET, setOf()))
+ }
+
+ private fun migrateVmessBean(angConfig: AngConfig, sharedPreferences: SharedPreferences) {
+ angConfig.vmess.forEachIndexed { index, vmessBean ->
+ val type = EConfigType.fromInt(vmessBean.configType) ?: return@forEachIndexed
+ val config = ServerConfig.create(type)
+ config.remarks = vmessBean.remarks
+ config.subscriptionId = vmessBean.subid
+ if (type == EConfigType.CUSTOM) {
+ val jsonConfig = sharedPreferences.getString(ANG_CONFIG + vmessBean.guid, "")
+ val v2rayConfig = try {
+ Gson().fromJson(jsonConfig, V2rayConfig::class.java)
+ } catch (e: Exception) {
+ e.printStackTrace()
+ return@forEachIndexed
+ }
+ config.fullConfig = v2rayConfig
+ serverRawStorage?.encode(vmessBean.guid, jsonConfig)
+ } else {
+ config.outboundBean?.settings?.vnext?.get(0)?.let { vnext ->
+ vnext.address = vmessBean.address
+ vnext.port = vmessBean.port
+ vnext.users[0].id = vmessBean.id
+ if (config.configType == EConfigType.VMESS) {
+ vnext.users[0].alterId = vmessBean.alterId
+ vnext.users[0].security = vmessBean.security
+ } else if (config.configType == EConfigType.VLESS) {
+ vnext.users[0].encryption = vmessBean.security
+ vnext.users[0].flow = vmessBean.flow
+ }
+ }
+ config.outboundBean?.settings?.servers?.get(0)?.let { server ->
+ server.address = vmessBean.address
+ server.port = vmessBean.port
+ if (config.configType == EConfigType.SHADOWSOCKS) {
+ server.password = vmessBean.id
+ server.method = vmessBean.security
+ } else if (config.configType == EConfigType.SOCKS) {
+ if (TextUtils.isEmpty(vmessBean.security) && TextUtils.isEmpty(vmessBean.id)) {
+ server.users = null
+ } else {
+ val socksUsersBean = V2rayConfig.OutboundBean.OutSettingsBean.ServersBean.SocksUsersBean()
+ socksUsersBean.user = vmessBean.security
+ socksUsersBean.pass = vmessBean.id
+ server.users = listOf(socksUsersBean)
+ }
+ } else if (config.configType == EConfigType.TROJAN) {
+ server.password = vmessBean.id
+ }
+ }
+ config.outboundBean?.streamSettings?.let { streamSetting ->
+ val sni = streamSetting.populateTransportSettings(vmessBean.network, vmessBean.headerType,
+ vmessBean.requestHost, vmessBean.path, vmessBean.path, vmessBean.requestHost, vmessBean.path,
+ vmessBean.headerType, vmessBean.path)
+ val allowInsecure = if (vmessBean.allowInsecure.isBlank()) {
+ settingsStorage?.decodeBool(AppConfig.PREF_ALLOW_INSECURE) ?: false
+ } else {
+ vmessBean.allowInsecure.toBoolean()
+ }
+ var fingerprint = streamSetting.tlsSettings?.fingerprint
+ streamSetting.populateTlsSettings(vmessBean.streamSecurity, allowInsecure,
+ vmessBean.sni.ifBlank { sni }, fingerprint, null)
+ }
+ }
+ val key = MmkvManager.encodeServerConfig(vmessBean.guid, config)
+ if (index == angConfig.index) {
+ mainStorage?.encode(KEY_SELECTED_SERVER, key)
+ }
+ }
+ }
+
+ private fun migrateSubItemBean(angConfig: AngConfig) {
+ angConfig.subItem.forEach {
+ val subItem = SubscriptionItem()
+ subItem.remarks = it.remarks
+ subItem.url = it.url
+ subItem.enabled = it.enabled
+ subStorage?.encode(it.id, Gson().toJson(subItem))
+ }
+ }
+
+ /**
+ * import config form qrcode or...
+ */
+ private fun importConfig(str: String?, subid: String, removedSelectedServer: ServerConfig?): Int {
+ try {
+ if (str == null || TextUtils.isEmpty(str)) {
+ return R.string.toast_none_data
+ }
+
+ //maybe sub
+ if (TextUtils.isEmpty(subid) && (str.startsWith(HTTP_PROTOCOL) || str.startsWith(HTTPS_PROTOCOL))) {
+ MmkvManager.importUrlAsSubscription(str)
+ return 0
+ }
+
+ var config: ServerConfig? = null
+ val allowInsecure = settingsStorage?.decodeBool(AppConfig.PREF_ALLOW_INSECURE) ?: false
+ if (str.startsWith(EConfigType.VMESS.protocolScheme)) {
+ config = ServerConfig.create(EConfigType.VMESS)
+ val streamSetting = config.outboundBean?.streamSettings ?: return -1
+
+ var fingerprint = streamSetting.tlsSettings?.fingerprint
+
+
+ if (!tryParseNewVmess(str, config, allowInsecure)) {
+ if (str.indexOf("?") > 0) {
+ if (!tryResolveVmess4Kitsunebi(str, config)) {
+ return R.string.toast_incorrect_protocol
+ }
+ } else {
+ var result = str.replace(EConfigType.VMESS.protocolScheme, "")
+ result = Utils.decode(result)
+ if (TextUtils.isEmpty(result)) {
+ return R.string.toast_decoding_failed
+ }
+ val vmessQRCode = Gson().fromJson(result, VmessQRCode::class.java)
+ // Although VmessQRCode fields are non null, looks like Gson may still create null fields
+ if (TextUtils.isEmpty(vmessQRCode.add)
+ || TextUtils.isEmpty(vmessQRCode.port)
+ || TextUtils.isEmpty(vmessQRCode.id)
+ || TextUtils.isEmpty(vmessQRCode.net)
+ ) {
+ return R.string.toast_incorrect_protocol
+ }
+
+ config.remarks = vmessQRCode.ps
+ config.outboundBean?.settings?.vnext?.get(0)?.let { vnext ->
+ vnext.address = vmessQRCode.add
+ vnext.port = Utils.parseInt(vmessQRCode.port)
+ vnext.users[0].id = vmessQRCode.id
+ vnext.users[0].security = if (TextUtils.isEmpty(vmessQRCode.scy)) DEFAULT_SECURITY else vmessQRCode.scy
+ vnext.users[0].alterId = Utils.parseInt(vmessQRCode.aid)
+ }
+ val sni = streamSetting.populateTransportSettings(vmessQRCode.net, vmessQRCode.type, vmessQRCode.host,
+ vmessQRCode.path, vmessQRCode.path, vmessQRCode.host, vmessQRCode.path, vmessQRCode.type, vmessQRCode.path)
+
+
+ streamSetting.populateTlsSettings(vmessQRCode.tls, allowInsecure,
+ if (TextUtils.isEmpty(vmessQRCode.sni)) sni else vmessQRCode.sni, fingerprint, vmessQRCode.alpn)
+ }
+ }
+ } else if (str.startsWith(EConfigType.SHADOWSOCKS.protocolScheme)) {
+ config = ServerConfig.create(EConfigType.SHADOWSOCKS)
+ if (!tryResolveResolveSip002(str, config)) {
+ var result = str.replace(EConfigType.SHADOWSOCKS.protocolScheme, "")
+ val indexSplit = result.indexOf("#")
+ if (indexSplit > 0) {
+ try {
+ config.remarks = Utils.urlDecode(result.substring(indexSplit + 1, result.length))
+ } catch (e: Exception) {
+ e.printStackTrace()
+ }
+
+ result = result.substring(0, indexSplit)
+ }
+
+ //part decode
+ val indexS = result.indexOf("@")
+ result = if (indexS > 0) {
+ Utils.decode(result.substring(0, indexS)) + result.substring(indexS, result.length)
+ } else {
+ Utils.decode(result)
+ }
+
+ val legacyPattern = "^(.+?):(.*)@(.+?):(\\d+?)/?$".toRegex()
+ val match = legacyPattern.matchEntire(result) ?: return R.string.toast_incorrect_protocol
+
+ config.outboundBean?.settings?.servers?.get(0)?.let { server ->
+ server.address = match.groupValues[3].removeSurrounding("[", "]")
+ server.port = match.groupValues[4].toInt()
+ server.password = match.groupValues[2]
+ server.method = match.groupValues[1].lowercase()
+ }
+ }
+ } else if (str.startsWith(EConfigType.SOCKS.protocolScheme)) {
+ var result = str.replace(EConfigType.SOCKS.protocolScheme, "")
+ val indexSplit = result.indexOf("#")
+ config = ServerConfig.create(EConfigType.SOCKS)
+ if (indexSplit > 0) {
+ try {
+ config.remarks = Utils.urlDecode(result.substring(indexSplit + 1, result.length))
+ } catch (e: Exception) {
+ e.printStackTrace()
+ }
+
+ result = result.substring(0, indexSplit)
+ }
+
+ //part decode
+ val indexS = result.indexOf("@")
+ if (indexS > 0) {
+ result = Utils.decode(result.substring(0, indexS)) + result.substring(indexS, result.length)
+ } else {
+ result = Utils.decode(result)
+ }
+
+ val legacyPattern = "^(.*):(.*)@(.+?):(\\d+?)$".toRegex()
+ val match = legacyPattern.matchEntire(result) ?: return R.string.toast_incorrect_protocol
+
+ config.outboundBean?.settings?.servers?.get(0)?.let { server ->
+ server.address = match.groupValues[3].removeSurrounding("[", "]")
+ server.port = match.groupValues[4].toInt()
+ val socksUsersBean = V2rayConfig.OutboundBean.OutSettingsBean.ServersBean.SocksUsersBean()
+ socksUsersBean.user = match.groupValues[1].lowercase()
+ socksUsersBean.pass = match.groupValues[2]
+ server.users = listOf(socksUsersBean)
+ }
+ } else if (str.startsWith(EConfigType.TROJAN.protocolScheme)) {
+ val uri = URI(Utils.fixIllegalUrl(str))
+ config = ServerConfig.create(EConfigType.TROJAN)
+ config.remarks = Utils.urlDecode(uri.fragment ?: "")
+
+ var flow = ""
+ var fingerprint = config.outboundBean?.streamSettings?.tlsSettings?.fingerprint
+ if (uri.rawQuery != null) {
+ val queryParam = uri.rawQuery.split("&")
+ .associate { it.split("=").let { (k, v) -> k to Utils.urlDecode(v) } }
+
+ val sni = config.outboundBean?.streamSettings?.populateTransportSettings(queryParam["type"] ?: "tcp", queryParam["headerType"],
+ queryParam["host"], queryParam["path"], queryParam["seed"], queryParam["quicSecurity"], queryParam["key"],
+ queryParam["mode"], queryParam["serviceName"])
+ config.outboundBean?.streamSettings?.populateTlsSettings(queryParam["security"] ?: TLS, allowInsecure, queryParam["sni"] ?: sni!!, fingerprint, queryParam["alpn"])
+ flow = queryParam["flow"] ?: ""
+ } else {
+
+ config.outboundBean?.streamSettings?.populateTlsSettings(TLS, allowInsecure, "", fingerprint, null)
+ }
+
+ config.outboundBean?.settings?.servers?.get(0)?.let { server ->
+ server.address = uri.idnHost
+ server.port = uri.port
+ server.password = uri.userInfo
+ server.flow = flow
+ }
+ } else if (str.startsWith(EConfigType.VLESS.protocolScheme)) {
+ val uri = URI(Utils.fixIllegalUrl(str))
+ val queryParam = uri.rawQuery.split("&")
+ .associate { it.split("=").let { (k, v) -> k to Utils.urlDecode(v) } }
+ config = ServerConfig.create(EConfigType.VLESS)
+ val streamSetting = config.outboundBean?.streamSettings ?: return -1
+ var fingerprint = streamSetting.tlsSettings?.fingerprint
+
+ config.remarks = Utils.urlDecode(uri.fragment ?: "")
+ config.outboundBean?.settings?.vnext?.get(0)?.let { vnext ->
+ vnext.address = uri.idnHost
+ vnext.port = uri.port
+ vnext.users[0].id = uri.userInfo
+ vnext.users[0].encryption = queryParam["encryption"] ?: "none"
+ vnext.users[0].flow =queryParam["flow"] ?: ""
+ }
+
+ val sni = streamSetting.populateTransportSettings(queryParam["type"] ?: "tcp", queryParam["headerType"],
+ queryParam["host"], queryParam["path"], queryParam["seed"], queryParam["quicSecurity"], queryParam["key"],
+ queryParam["mode"], queryParam["serviceName"])
+ streamSetting.populateTlsSettings(queryParam["security"] ?: "", allowInsecure, queryParam["sni"] ?: sni, fingerprint, queryParam["alpn"])
+ }
+ if (config == null){
+ return R.string.toast_incorrect_protocol
+ }
+ config.subscriptionId = subid
+ val guid = MmkvManager.encodeServerConfig("", config)
+ if (removedSelectedServer != null &&
+ config.getProxyOutbound()?.getServerAddress() == removedSelectedServer.getProxyOutbound()?.getServerAddress() &&
+ config.getProxyOutbound()?.getServerPort() == removedSelectedServer.getProxyOutbound()?.getServerPort()) {
+ mainStorage?.encode(KEY_SELECTED_SERVER, guid)
+ }
+ } catch (e: Exception) {
+ e.printStackTrace()
+ return -1
+ }
+ return 0
+ }
+
+ private fun tryParseNewVmess(uriString: String, config: ServerConfig, allowInsecure: Boolean): Boolean {
+ return runCatching {
+ val uri = URI(uriString)
+ check(uri.scheme == "vmess")
+ val (_, protocol, tlsStr, uuid, alterId) =
+ Regex("(tcp|http|ws|kcp|quic|grpc)(\\+tls)?:([0-9a-z]{8}-[0-9a-z]{4}-[0-9a-z]{4}-[0-9a-z]{4}-[0-9a-z]{12})")
+ .matchEntire(uri.userInfo)?.groupValues
+ ?: error("parse user info fail.")
+ val tls = tlsStr.isNotBlank()
+ val queryParam = uri.rawQuery.split("&")
+ .associate { it.split("=").let { (k, v) -> k to Utils.urlDecode(v) } }
+
+ val streamSetting = config.outboundBean?.streamSettings ?: return false
+ config.remarks = Utils.urlDecode(uri.fragment ?: "")
+ config.outboundBean.settings?.vnext?.get(0)?.let { vnext ->
+ vnext.address = uri.idnHost
+ vnext.port = uri.port
+ vnext.users[0].id = uuid
+ vnext.users[0].security = DEFAULT_SECURITY
+ vnext.users[0].alterId = alterId.toInt()
+ }
+ var fingerprint = streamSetting.tlsSettings?.fingerprint
+ val sni = streamSetting.populateTransportSettings(protocol, queryParam["type"],
+ queryParam["host"]?.split("|")?.get(0) ?: "",
+ queryParam["path"]?.takeIf { it.trim() != "/" } ?: "", queryParam["seed"], queryParam["security"],
+ queryParam["key"], queryParam["mode"], queryParam["serviceName"])
+ streamSetting.populateTlsSettings(if (tls) TLS else "", allowInsecure, sni, fingerprint, null)
+ true
+ }.getOrElse { false }
+ }
+
+ private fun tryResolveVmess4Kitsunebi(server: String, config: ServerConfig): Boolean {
+
+ var result = server.replace(EConfigType.VMESS.protocolScheme, "")
+ val indexSplit = result.indexOf("?")
+ if (indexSplit > 0) {
+ result = result.substring(0, indexSplit)
+ }
+ result = Utils.decode(result)
+
+ val arr1 = result.split('@')
+ if (arr1.count() != 2) {
+ return false
+ }
+ val arr21 = arr1[0].split(':')
+ val arr22 = arr1[1].split(':')
+ if (arr21.count() != 2) {
+ return false
+ }
+
+ config.remarks = "Alien"
+ config.outboundBean?.settings?.vnext?.get(0)?.let { vnext ->
+ vnext.address = arr22[0]
+ vnext.port = Utils.parseInt(arr22[1])
+ vnext.users[0].id = arr21[1]
+ vnext.users[0].security = arr21[0]
+ vnext.users[0].alterId = 0
+ }
+ return true
+ }
+
+ private fun tryResolveResolveSip002(str: String, config: ServerConfig): Boolean {
+ try {
+ val uri = URI(Utils.fixIllegalUrl(str))
+ config.remarks = Utils.urlDecode(uri.fragment ?: "")
+
+ val method: String
+ val password: String
+ if (uri.userInfo.contains(":")) {
+ val arrUserInfo = uri.userInfo.split(":").map { it.trim() }
+ if (arrUserInfo.count() != 2) {
+ return false
+ }
+ method = arrUserInfo[0]
+ password = Utils.urlDecode(arrUserInfo[1])
+ } else {
+ val base64Decode = Utils.decode(uri.userInfo)
+ val arrUserInfo = base64Decode.split(":").map { it.trim() }
+ if (arrUserInfo.count() < 2) {
+ return false
+ }
+ method = arrUserInfo[0]
+ password = base64Decode.substringAfter(":")
+ }
+
+ config.outboundBean?.settings?.servers?.get(0)?.let { server ->
+ server.address = uri.idnHost
+ server.port = uri.port
+ server.password = password
+ server.method = method
+ }
+ return true
+ } catch (e: Exception) {
+ Log.d(AppConfig.ANG_PACKAGE, e.toString())
+ return false
+ }
+ }
+
+ /**
+ * share config
+ */
+ private fun shareConfig(guid: String): String {
+ try {
+ val config = MmkvManager.decodeServerConfig(guid) ?: return ""
+ val outbound = config.getProxyOutbound() ?: return ""
+ val streamSetting = outbound.streamSettings ?: return ""
+ return config.configType.protocolScheme + when (config.configType) {
+ EConfigType.VMESS -> {
+ val vmessQRCode = VmessQRCode()
+ vmessQRCode.v = "2"
+ vmessQRCode.ps = config.remarks
+ vmessQRCode.add = outbound.getServerAddress().orEmpty()
+ vmessQRCode.port = outbound.getServerPort().toString()
+ vmessQRCode.id = outbound.getPassword().orEmpty()
+ vmessQRCode.aid = outbound.settings?.vnext?.get(0)?.users?.get(0)?.alterId.toString()
+ vmessQRCode.scy = outbound.settings?.vnext?.get(0)?.users?.get(0)?.security.toString()
+ vmessQRCode.net = streamSetting.network
+ vmessQRCode.tls = streamSetting.security
+ vmessQRCode.sni = streamSetting.tlsSettings?.serverName.orEmpty()
+ vmessQRCode.alpn = Utils.removeWhiteSpace(streamSetting.tlsSettings?.alpn?.joinToString()).orEmpty()
+ outbound.getTransportSettingDetails()?.let { transportDetails ->
+ vmessQRCode.type = transportDetails[0]
+ vmessQRCode.host = transportDetails[1]
+ vmessQRCode.path = transportDetails[2]
+ }
+ val json = Gson().toJson(vmessQRCode)
+ Utils.encode(json)
+ }
+ EConfigType.CUSTOM, EConfigType.WIREGUARD -> ""
+ EConfigType.SHADOWSOCKS -> {
+ val remark = "#" + Utils.urlEncode(config.remarks)
+ val pw = Utils.encode("${outbound.getSecurityEncryption()}:${outbound.getPassword()}")
+ val url = String.format("%s@%s:%s",
+ pw,
+ Utils.getIpv6Address(outbound.getServerAddress()!!),
+ outbound.getServerPort())
+ url + remark
+ }
+ EConfigType.SOCKS -> {
+ val remark = "#" + Utils.urlEncode(config.remarks)
+ val pw = Utils.encode("${outbound.settings?.servers?.get(0)?.users?.get(0)?.user}:${outbound.getPassword()}")
+ val url = String.format("%s@%s:%s",
+ pw,
+ Utils.getIpv6Address(outbound.getServerAddress()!!),
+ outbound.getServerPort())
+ url + remark
+ }
+ EConfigType.VLESS,
+ EConfigType.TROJAN -> {
+ val remark = "#" + Utils.urlEncode(config.remarks)
+
+ val dicQuery = HashMap()
+ if (config.configType == EConfigType.VLESS) {
+ outbound.settings?.vnext?.get(0)?.users?.get(0)?.flow?.let {
+ if (!TextUtils.isEmpty(it)) {
+ dicQuery["flow"] = it
+ }
+ }
+ dicQuery["encryption"] =
+ if (outbound.getSecurityEncryption().isNullOrEmpty()) "none"
+ else outbound.getSecurityEncryption().orEmpty()
+ } else if (config.configType == EConfigType.TROJAN) {
+ config.outboundBean?.settings?.servers?.get(0)?.flow?.let {
+ if (!TextUtils.isEmpty(it)) {
+ dicQuery["flow"] = it
+ }
+ }
+ }
+
+ dicQuery["security"] = streamSetting.security.ifEmpty { "none" }
+ (streamSetting.tlsSettings?: streamSetting.xtlsSettings)?.let { tlsSetting ->
+ if (!TextUtils.isEmpty(tlsSetting.serverName)) {
+ dicQuery["sni"] = tlsSetting.serverName
+ }
+ if (!tlsSetting.alpn.isNullOrEmpty() && tlsSetting.alpn.isNotEmpty()) {
+ dicQuery["alpn"] = Utils.removeWhiteSpace(tlsSetting.alpn.joinToString()).orEmpty()
+ }
+ }
+ dicQuery["type"] = streamSetting.network.ifEmpty { V2rayConfig.DEFAULT_NETWORK }
+
+ outbound.getTransportSettingDetails()?.let { transportDetails ->
+ when (streamSetting.network) {
+ "tcp" -> {
+ dicQuery["headerType"] = transportDetails[0].ifEmpty { "none" }
+ if (!TextUtils.isEmpty(transportDetails[1])) {
+ dicQuery["host"] = Utils.urlEncode(transportDetails[1])
+ }
+ }
+ "kcp" -> {
+ dicQuery["headerType"] = transportDetails[0].ifEmpty { "none" }
+ if (!TextUtils.isEmpty(transportDetails[2])) {
+ dicQuery["seed"] = Utils.urlEncode(transportDetails[2])
+ }
+ }
+ "ws" -> {
+ if (!TextUtils.isEmpty(transportDetails[1])) {
+ dicQuery["host"] = Utils.urlEncode(transportDetails[1])
+ }
+ if (!TextUtils.isEmpty(transportDetails[2])) {
+ dicQuery["path"] = Utils.urlEncode(transportDetails[2])
+ }
+ }
+ "http", "h2" -> {
+ dicQuery["type"] = "http"
+ if (!TextUtils.isEmpty(transportDetails[1])) {
+ dicQuery["host"] = Utils.urlEncode(transportDetails[1])
+ }
+ if (!TextUtils.isEmpty(transportDetails[2])) {
+ dicQuery["path"] = Utils.urlEncode(transportDetails[2])
+ }
+ }
+ "quic" -> {
+ dicQuery["headerType"] = transportDetails[0].ifEmpty { "none" }
+ dicQuery["quicSecurity"] = Utils.urlEncode(transportDetails[1])
+ dicQuery["key"] = Utils.urlEncode(transportDetails[2])
+ }
+ "grpc" -> {
+ dicQuery["mode"] = transportDetails[0]
+ dicQuery["serviceName"] = transportDetails[2]
+ }
+ }
+ }
+ val query = "?" + dicQuery.toList().joinToString(
+ separator = "&",
+ transform = { it.first + "=" + it.second })
+
+ val url = String.format("%s@%s:%s",
+ outbound.getPassword(),
+ Utils.getIpv6Address(outbound.getServerAddress()!!),
+ outbound.getServerPort())
+ url + query + remark
+ }
+ }
+ } catch (e: Exception) {
+ e.printStackTrace()
+ return ""
+ }
+ }
+
+ /**
+ * share2Clipboard
+ */
+ fun share2Clipboard(context: Context, guid: String): Int {
+ try {
+ val conf = shareConfig(guid)
+ if (TextUtils.isEmpty(conf)) {
+ return -1
+ }
+
+ Utils.setClipboard(context, conf)
+
+ } catch (e: Exception) {
+ e.printStackTrace()
+ return -1
+ }
+ return 0
+ }
+
+ /**
+ * share2Clipboard
+ */
+ fun shareNonCustomConfigsToClipboard(context: Context, serverList: List): Int {
+ try {
+ val sb = StringBuilder()
+ for (guid in serverList) {
+ val url = shareConfig(guid)
+ if (TextUtils.isEmpty(url)) {
+ continue
+ }
+ sb.append(url)
+ sb.appendLine()
+ }
+ if (sb.count() > 0) {
+ Utils.setClipboard(context, sb.toString())
+ }
+ } catch (e: Exception) {
+ e.printStackTrace()
+ return -1
+ }
+ return 0
+ }
+
+ /**
+ * share2QRCode
+ */
+ fun share2QRCode(guid: String): Bitmap? {
+ try {
+ val conf = shareConfig(guid)
+ if (TextUtils.isEmpty(conf)) {
+ return null
+ }
+ return Utils.createQRCode(conf)
+
+ } catch (e: Exception) {
+ e.printStackTrace()
+ return null
+ }
+ }
+
+ /**
+ * shareFullContent2Clipboard
+ */
+ fun shareFullContent2Clipboard(context: Context, guid: String?): Int {
+ try {
+ if (guid == null) return -1
+ val result = V2rayConfigUtil.getV2rayConfig(context, guid)
+ if (result.status) {
+ Utils.setClipboard(context, result.content)
+ } else {
+ return -1
+ }
+ } catch (e: Exception) {
+ e.printStackTrace()
+ return -1
+ }
+ return 0
+ }
+
+ /**
+ * upgrade
+ */
+ private fun upgradeServerVersion(vmess: AngConfig.VmessBean): Int {
+ try {
+ if (vmess.configVersion == 2) {
+ return 0
+ }
+
+ when (vmess.network) {
+ "ws", "h2" -> {
+ var path = ""
+ var host = ""
+ val lstParameter = vmess.requestHost.split(";")
+ if (lstParameter.isNotEmpty()) {
+ path = lstParameter[0].trim()
+ }
+ if (lstParameter.size > 1) {
+ path = lstParameter[0].trim()
+ host = lstParameter[1].trim()
+ }
+ vmess.path = path
+ vmess.requestHost = host
+ }
+ }
+ vmess.configVersion = 2
+ return 0
+ } catch (e: Exception) {
+ e.printStackTrace()
+ return -1
+ }
+ }
+
+ fun importBatchConfig(servers: String?, subid: String, append: Boolean): Int {
+ try {
+ if (servers == null) {
+ return 0
+ }
+ val removedSelectedServer =
+ if (!TextUtils.isEmpty(subid) && !append) {
+ MmkvManager.decodeServerConfig(mainStorage?.decodeString(KEY_SELECTED_SERVER) ?: "")?.let {
+ if (it.subscriptionId == subid) {
+ return@let it
+ }
+ return@let null
+ }
+ } else {
+ null
+ }
+ if(!append) {
+ MmkvManager.removeServerViaSubid(subid)
+ }
+// var servers = server
+// if (server.indexOf("vmess") >= 0 && server.indexOf("vmess") == server.lastIndexOf("vmess")) {
+// servers = server.replace("\n", "")
+// }
+
+ var count = 0
+ servers.lines()
+ .forEach {
+ val resId = importConfig(it, subid, removedSelectedServer)
+ if (resId == 0) {
+ count++
+ }
+ }
+ return count
+ } catch (e: Exception) {
+ e.printStackTrace()
+ }
+ return 0
+ }
+}
diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/AppManagerUtil.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/AppManagerUtil.kt
new file mode 100644
index 00000000..b95b408b
--- /dev/null
+++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/AppManagerUtil.kt
@@ -0,0 +1,43 @@
+package com.v2ray.ang.util
+
+import android.Manifest
+import android.content.Context
+import android.content.pm.ApplicationInfo
+import android.content.pm.PackageInfo
+import android.content.pm.PackageManager
+import com.v2ray.ang.dto.AppInfo
+import rx.Observable
+import java.util.*
+
+object AppManagerUtil {
+ fun loadNetworkAppList(ctx: Context): ArrayList {
+ val packageManager = ctx.packageManager
+ val packages = packageManager.getInstalledPackages(PackageManager.GET_PERMISSIONS)
+ val apps = ArrayList()
+
+ for (pkg in packages) {
+ if (!pkg.hasInternetPermission && pkg.packageName != "android") continue
+
+ val applicationInfo = pkg.applicationInfo
+
+ val appName = applicationInfo.loadLabel(packageManager).toString()
+ val appIcon = applicationInfo.loadIcon(packageManager)
+ val isSystemApp = (applicationInfo.flags and ApplicationInfo.FLAG_SYSTEM) > 0
+
+ val appInfo = AppInfo(appName, pkg.packageName, appIcon, isSystemApp, 0)
+ apps.add(appInfo)
+ }
+
+ return apps
+ }
+
+ fun rxLoadNetworkAppList(ctx: Context): Observable> = Observable.unsafeCreate {
+ it.onNext(loadNetworkAppList(ctx))
+ }
+
+ val PackageInfo.hasInternetPermission: Boolean
+ get() {
+ val permissions = requestedPermissions
+ return permissions?.any { it == Manifest.permission.INTERNET } ?: false
+ }
+}
diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/MessageUtil.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/MessageUtil.kt
new file mode 100644
index 00000000..0f521f57
--- /dev/null
+++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/MessageUtil.kt
@@ -0,0 +1,45 @@
+package com.v2ray.ang.util
+
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import com.v2ray.ang.AppConfig
+import com.v2ray.ang.service.V2RayTestService
+import java.io.Serializable
+
+
+object MessageUtil {
+
+ fun sendMsg2Service(ctx: Context, what: Int, content: Serializable) {
+ sendMsg(ctx, AppConfig.BROADCAST_ACTION_SERVICE, what, content)
+ }
+
+ fun sendMsg2UI(ctx: Context, what: Int, content: Serializable) {
+ sendMsg(ctx, AppConfig.BROADCAST_ACTION_ACTIVITY, what, content)
+ }
+
+ fun sendMsg2TestService(ctx: Context, what: Int, content: Serializable) {
+ try {
+ val intent = Intent()
+ intent.component = ComponentName(ctx, V2RayTestService::class.java)
+ intent.putExtra("key", what)
+ intent.putExtra("content", content)
+ ctx.startService(intent)
+ } catch (e: Exception) {
+ e.printStackTrace()
+ }
+ }
+
+ private fun sendMsg(ctx: Context, action: String, what: Int, content: Serializable) {
+ try {
+ val intent = Intent()
+ intent.action = action
+ intent.`package` = AppConfig.ANG_PACKAGE
+ intent.putExtra("key", what)
+ intent.putExtra("content", content)
+ ctx.sendBroadcast(intent)
+ } catch (e: Exception) {
+ e.printStackTrace()
+ }
+ }
+}
diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/MmkvManager.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/MmkvManager.kt
new file mode 100644
index 00000000..533527e7
--- /dev/null
+++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/MmkvManager.kt
@@ -0,0 +1,179 @@
+package com.v2ray.ang.util
+
+import com.google.gson.Gson
+import com.tencent.mmkv.MMKV
+import com.v2ray.ang.dto.ServerAffiliationInfo
+import com.v2ray.ang.dto.ServerConfig
+import com.v2ray.ang.dto.SubscriptionItem
+
+object MmkvManager {
+ const val ID_MAIN = "MAIN"
+ const val ID_SERVER_CONFIG = "SERVER_CONFIG"
+ const val ID_SERVER_RAW = "SERVER_RAW"
+ const val ID_SERVER_AFF = "SERVER_AFF"
+ const val ID_SUB = "SUB"
+ const val ID_SETTING = "SETTING"
+ const val KEY_SELECTED_SERVER = "SELECTED_SERVER"
+ const val KEY_ANG_CONFIGS = "ANG_CONFIGS"
+
+ private val mainStorage by lazy { MMKV.mmkvWithID(ID_MAIN, MMKV.MULTI_PROCESS_MODE) }
+ private val serverStorage by lazy { MMKV.mmkvWithID(ID_SERVER_CONFIG, MMKV.MULTI_PROCESS_MODE) }
+ private val serverAffStorage by lazy { MMKV.mmkvWithID(ID_SERVER_AFF, MMKV.MULTI_PROCESS_MODE) }
+ private val subStorage by lazy { MMKV.mmkvWithID(ID_SUB, MMKV.MULTI_PROCESS_MODE) }
+
+ fun decodeServerList(): MutableList {
+ val json = mainStorage?.decodeString(KEY_ANG_CONFIGS)
+ return if (json.isNullOrBlank()) {
+ mutableListOf()
+ } else {
+ Gson().fromJson(json, Array::class.java).toMutableList()
+ }
+ }
+
+ fun decodeServerConfig(guid: String): ServerConfig? {
+ if (guid.isBlank()) {
+ return null
+ }
+ val json = serverStorage?.decodeString(guid)
+ if (json.isNullOrBlank()) {
+ return null
+ }
+ return Gson().fromJson(json, ServerConfig::class.java)
+ }
+
+ fun encodeServerConfig(guid: String, config: ServerConfig): String {
+ val key = guid.ifBlank { Utils.getUuid() }
+ serverStorage?.encode(key, Gson().toJson(config))
+ val serverList = decodeServerList()
+ if (!serverList.contains(key)) {
+ serverList.add(key)
+ mainStorage?.encode(KEY_ANG_CONFIGS, Gson().toJson(serverList))
+ if (mainStorage?.decodeString(KEY_SELECTED_SERVER).isNullOrBlank()) {
+ mainStorage?.encode(KEY_SELECTED_SERVER, key)
+ }
+ }
+ return key
+ }
+
+ fun removeServer(guid: String) {
+ if (guid.isBlank()) {
+ return
+ }
+ if (mainStorage?.decodeString(KEY_SELECTED_SERVER) == guid) {
+ mainStorage?.remove(KEY_SELECTED_SERVER)
+ }
+ val serverList = decodeServerList()
+ serverList.remove(guid)
+ mainStorage?.encode(KEY_ANG_CONFIGS, Gson().toJson(serverList))
+ serverStorage?.remove(guid)
+ serverAffStorage?.remove(guid)
+ }
+
+ fun removeServerViaSubid(subid: String) {
+ if (subid.isBlank()) {
+ return
+ }
+ serverStorage?.allKeys()?.forEach { key ->
+ decodeServerConfig(key)?.let { config ->
+ if (config.subscriptionId == subid) {
+ removeServer(key)
+ }
+ }
+ }
+ }
+
+ fun decodeServerAffiliationInfo(guid: String): ServerAffiliationInfo? {
+ if (guid.isBlank()) {
+ return null
+ }
+ val json = serverAffStorage?.decodeString(guid)
+ if (json.isNullOrBlank()) {
+ return null
+ }
+ return Gson().fromJson(json, ServerAffiliationInfo::class.java)
+ }
+
+ fun encodeServerTestDelayMillis(guid: String, testResult: Long) {
+ if (guid.isBlank()) {
+ return
+ }
+ val aff = decodeServerAffiliationInfo(guid) ?: ServerAffiliationInfo()
+ aff.testDelayMillis = testResult
+ serverAffStorage?.encode(guid, Gson().toJson(aff))
+ }
+
+ fun clearAllTestDelayResults() {
+ serverAffStorage?.allKeys()?.forEach { key ->
+ decodeServerAffiliationInfo(key)?.let { aff ->
+ aff.testDelayMillis = 0
+ serverAffStorage?.encode(key, Gson().toJson(aff))
+ }
+ }
+ }
+
+ fun importUrlAsSubscription(url: String): Int {
+ val subscriptions = decodeSubscriptions()
+ subscriptions.forEach {
+ if (it.second.url == url) {
+ return 0
+ }
+ }
+ val subItem = SubscriptionItem()
+ subItem.remarks = "import sub"
+ subItem.url = url
+ subStorage?.encode(Utils.getUuid(), Gson().toJson(subItem))
+ return 1
+ }
+
+ fun decodeSubscriptions(): List> {
+ val subscriptions = mutableListOf>()
+ subStorage?.allKeys()?.forEach { key ->
+ val json = subStorage?.decodeString(key)
+ if (!json.isNullOrBlank()) {
+ subscriptions.add(Pair(key, Gson().fromJson(json, SubscriptionItem::class.java)))
+ }
+ }
+ subscriptions.sortedBy { (_, value) -> value.addedTime }
+ return subscriptions
+ }
+
+ fun removeSubscription(subid: String) {
+ subStorage?.remove(subid)
+ removeServerViaSubid(subid)
+ }
+
+ fun removeAllServer() {
+ mainStorage?.clearAll()
+ serverStorage?.clearAll()
+ serverAffStorage?.clearAll()
+ }
+
+ fun removeInvalidServer() {
+ serverAffStorage?.allKeys()?.forEach { key ->
+ decodeServerAffiliationInfo(key)?.let { aff ->
+ if (aff.testDelayMillis <= 0L) {
+ removeServer(key)
+ }
+ }
+ }
+ }
+
+ fun sortByTestResults( ) {
+ data class ServerDelay(var guid: String, var testDelayMillis: Long)
+
+ val serverDelays = mutableListOf()
+ val serverList = decodeServerList()
+ serverList.forEach { key ->
+ val delay = decodeServerAffiliationInfo(key)?.testDelayMillis ?: 0L
+ serverDelays.add(ServerDelay(key, if (delay <= 0L) 999999 else delay))
+ }
+ serverDelays.sortBy { it.testDelayMillis }
+
+ serverDelays.forEach {
+ serverList.remove(it.guid)
+ serverList.add(it.guid)
+ }
+
+ mainStorage?.encode(KEY_ANG_CONFIGS, Gson().toJson(serverList))
+ }
+}
diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/MyContextWrapper.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/MyContextWrapper.kt
new file mode 100644
index 00000000..cf9bd35e
--- /dev/null
+++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/MyContextWrapper.kt
@@ -0,0 +1,33 @@
+package com.v2ray.ang.util
+
+import android.content.Context
+import android.content.ContextWrapper
+import android.content.res.Configuration
+import android.content.res.Resources
+import android.os.Build
+import android.os.LocaleList
+import androidx.annotation.RequiresApi
+import java.util.*
+
+open class MyContextWrapper(base: Context?) : ContextWrapper(base) {
+ companion object {
+ @RequiresApi(Build.VERSION_CODES.N)
+ fun wrap(context: Context, newLocale: Locale?): ContextWrapper {
+ var mContext = context
+ val res: Resources = mContext.resources
+ val configuration: Configuration = res.configuration
+ //注意 Android 7.0 前后的不同处理方法
+ mContext = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+ configuration.setLocale(newLocale)
+ val localeList = LocaleList(newLocale)
+ LocaleList.setDefault(localeList)
+ configuration.setLocales(localeList)
+ mContext.createConfigurationContext(configuration)
+ } else {
+ configuration.setLocale(newLocale)
+ mContext.createConfigurationContext(configuration)
+ }
+ return ContextWrapper(mContext)
+ }
+ }
+}
\ No newline at end of file
diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/QRCodeDecoder.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/QRCodeDecoder.kt
new file mode 100644
index 00000000..250f84df
--- /dev/null
+++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/QRCodeDecoder.kt
@@ -0,0 +1,100 @@
+package com.v2ray.ang.util
+
+import android.graphics.Bitmap
+import android.graphics.BitmapFactory
+import com.google.zxing.*
+import com.google.zxing.common.GlobalHistogramBinarizer
+import com.google.zxing.common.HybridBinarizer
+import java.util.*
+
+/**
+ * 描述:解析二维码图片
+ */
+object QRCodeDecoder {
+ val HINTS: MutableMap = EnumMap(DecodeHintType::class.java)
+
+ /**
+ * 同步解析本地图片二维码。该方法是耗时操作,请在子线程中调用。
+ *
+ * @param picturePath 要解析的二维码图片本地路径
+ * @return 返回二维码图片里的内容 或 null
+ */
+ fun syncDecodeQRCode(picturePath: String): String? {
+ return syncDecodeQRCode(getDecodeAbleBitmap(picturePath))
+ }
+
+ /**
+ * 同步解析bitmap二维码。该方法是耗时操作,请在子线程中调用。
+ *
+ * @param bitmap 要解析的二维码图片
+ * @return 返回二维码图片里的内容 或 null
+ */
+ fun syncDecodeQRCode(bitmap: Bitmap?): String? {
+ var source: RGBLuminanceSource? = null
+ try {
+ val width = bitmap!!.width
+ val height = bitmap.height
+ val pixels = IntArray(width * height)
+ bitmap.getPixels(pixels, 0, width, 0, 0, width, height)
+ source = RGBLuminanceSource(width, height, pixels)
+ return MultiFormatReader().decode(BinaryBitmap(HybridBinarizer(source)), HINTS).text
+ } catch (e: Exception) {
+ e.printStackTrace()
+ }
+ if (source != null) {
+ try {
+ return MultiFormatReader().decode(BinaryBitmap(GlobalHistogramBinarizer(source)), HINTS).text
+ } catch (e2: Throwable) {
+ e2.printStackTrace()
+ }
+ }
+ return null
+ }
+
+ /**
+ * 将本地图片文件转换成可解码二维码的 Bitmap。为了避免图片太大,这里对图片进行了压缩。感谢 https://github.com/devilsen 提的 PR
+ *
+ * @param picturePath 本地图片文件路径
+ * @return
+ */
+ private fun getDecodeAbleBitmap(picturePath: String): Bitmap? {
+ return try {
+ val options = BitmapFactory.Options()
+ options.inJustDecodeBounds = true
+ BitmapFactory.decodeFile(picturePath, options)
+ var sampleSize = options.outHeight / 400
+ if (sampleSize <= 0) {
+ sampleSize = 1
+ }
+ options.inSampleSize = sampleSize
+ options.inJustDecodeBounds = false
+ BitmapFactory.decodeFile(picturePath, options)
+ } catch (e: Exception) {
+ null
+ }
+ }
+
+ init {
+ val allFormats: List = arrayListOf(
+ BarcodeFormat.AZTEC
+ ,BarcodeFormat.CODABAR
+ ,BarcodeFormat.CODE_39
+ ,BarcodeFormat.CODE_93
+ ,BarcodeFormat.CODE_128
+ ,BarcodeFormat.DATA_MATRIX
+ ,BarcodeFormat.EAN_8
+ ,BarcodeFormat.EAN_13
+ ,BarcodeFormat.ITF
+ ,BarcodeFormat.MAXICODE
+ ,BarcodeFormat.PDF_417
+ ,BarcodeFormat.QR_CODE
+ ,BarcodeFormat.RSS_14
+ ,BarcodeFormat.RSS_EXPANDED
+ ,BarcodeFormat.UPC_A
+ ,BarcodeFormat.UPC_E
+ ,BarcodeFormat.UPC_EAN_EXTENSION)
+ HINTS[DecodeHintType.TRY_HARDER] = BarcodeFormat.QR_CODE
+ HINTS[DecodeHintType.POSSIBLE_FORMATS] = allFormats
+ HINTS[DecodeHintType.CHARACTER_SET] = "utf-8"
+ }
+}
diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/SpeedtestUtil.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/SpeedtestUtil.kt
new file mode 100644
index 00000000..5a0e5989
--- /dev/null
+++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/SpeedtestUtil.kt
@@ -0,0 +1,142 @@
+package com.v2ray.ang.util
+
+import android.content.Context
+import android.os.SystemClock
+import android.text.TextUtils
+import android.util.Log
+import com.v2ray.ang.AppConfig
+import com.v2ray.ang.R
+import com.v2ray.ang.extension.responseLength
+import kotlinx.coroutines.isActive
+import libv2ray.Libv2ray
+import java.io.IOException
+import java.net.*
+import java.util.*
+import kotlin.coroutines.coroutineContext
+
+object SpeedtestUtil {
+
+ private val tcpTestingSockets = ArrayList()
+
+ suspend fun tcping(url: String, port: Int): Long {
+ var time = -1L
+ for (k in 0 until 2) {
+ val one = socketConnectTime(url, port)
+ if (!coroutineContext.isActive) {
+ break
+ }
+ if (one != -1L && (time == -1L || one < time)) {
+ time = one
+ }
+ }
+ return time
+ }
+
+ fun realPing(config: String): Long {
+ return try {
+ Libv2ray.measureOutboundDelay(config)
+ } catch (e: Exception) {
+ Log.d(AppConfig.ANG_PACKAGE, "realPing: $e")
+ -1L
+ }
+ }
+
+ fun ping(url: String): String {
+ try {
+ val command = "/system/bin/ping -c 3 $url"
+ val process = Runtime.getRuntime().exec(command)
+ val allText = process.inputStream.bufferedReader().use { it.readText() }
+ if (!TextUtils.isEmpty(allText)) {
+ val tempInfo = allText.substring(allText.indexOf("min/avg/max/mdev") + 19)
+ val temps = tempInfo.split("/".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
+ if (temps.count() > 0 && temps[0].length < 10) {
+ return temps[0].toFloat().toInt().toString() + "ms"
+ }
+ }
+ } catch (e: Exception) {
+ e.printStackTrace()
+ }
+ return "-1ms"
+ }
+
+ fun socketConnectTime(url: String, port: Int): Long {
+ try {
+ val socket = Socket()
+ synchronized(this) {
+ tcpTestingSockets.add(socket)
+ }
+ val start = System.currentTimeMillis()
+ socket.connect(InetSocketAddress(url, port),3000)
+ val time = System.currentTimeMillis() - start
+ synchronized(this) {
+ tcpTestingSockets.remove(socket)
+ }
+ socket.close()
+ return time
+ } catch (e: UnknownHostException) {
+ e.printStackTrace()
+ } catch (e: IOException) {
+ Log.d(AppConfig.ANG_PACKAGE, "socketConnectTime IOException: $e")
+ } catch (e: Exception) {
+ e.printStackTrace()
+ }
+ return -1
+ }
+
+ fun closeAllTcpSockets() {
+ synchronized(this) {
+ tcpTestingSockets.forEach {
+ it?.close()
+ }
+ tcpTestingSockets.clear()
+ }
+ }
+
+ fun testConnection(context: Context, port: Int): String {
+ // return V2RayVpnService.measureV2rayDelay()
+ var result: String
+ var conn: HttpURLConnection? = null
+
+ try {
+ val url = URL("https",
+ "www.google.com",
+ "/generate_204")
+
+ conn = url.openConnection(
+ Proxy(Proxy.Type.HTTP,
+ InetSocketAddress("127.0.0.1", port))) as HttpURLConnection
+ conn.connectTimeout = 30000
+ conn.readTimeout = 30000
+ conn.setRequestProperty("Connection", "close")
+ conn.instanceFollowRedirects = false
+ conn.useCaches = false
+
+ val start = SystemClock.elapsedRealtime()
+ val code = conn.responseCode
+ val elapsed = SystemClock.elapsedRealtime() - start
+
+ if (code == 204 || code == 200 && conn.responseLength == 0L) {
+ result = context.getString(R.string.connection_test_available, elapsed)
+ } else {
+ throw IOException(context.getString(R.string.connection_test_error_status_code, code))
+ }
+ } catch (e: IOException) {
+ // network exception
+ Log.d(AppConfig.ANG_PACKAGE, "testConnection IOException: " + Log.getStackTraceString(e))
+ result = context.getString(R.string.connection_test_error, e.message)
+ } catch (e: Exception) {
+ // library exception, eg sumsung
+ Log.d(AppConfig.ANG_PACKAGE, "testConnection Exception: " + Log.getStackTraceString(e))
+ result = context.getString(R.string.connection_test_error, e.message)
+ } finally {
+ conn?.disconnect()
+ }
+
+ return result
+ }
+
+ fun getLibVersion(): String {
+ return Libv2ray.checkVersionX()
+ }
+
+}
diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/Utils.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/Utils.kt
new file mode 100644
index 00000000..48e9c488
--- /dev/null
+++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/Utils.kt
@@ -0,0 +1,439 @@
+package com.v2ray.ang.util
+
+import android.content.ClipboardManager
+import android.content.Context
+import android.text.Editable
+import android.util.Base64
+import com.google.zxing.WriterException
+import android.graphics.Bitmap
+import com.google.zxing.BarcodeFormat
+import com.google.zxing.qrcode.QRCodeWriter
+import com.google.zxing.EncodeHintType
+import java.util.*
+import kotlin.collections.HashMap
+import android.content.ClipData
+import android.content.Intent
+import android.content.res.Configuration.UI_MODE_NIGHT_MASK
+import android.content.res.Configuration.UI_MODE_NIGHT_YES
+import android.net.Uri
+import android.os.Build
+import android.os.LocaleList
+import android.util.Log
+import android.util.Patterns
+import android.webkit.URLUtil
+import com.tencent.mmkv.MMKV
+import com.v2ray.ang.AppConfig
+import com.v2ray.ang.AppConfig.ANG_PACKAGE
+import com.v2ray.ang.BuildConfig
+import com.v2ray.ang.R
+import com.v2ray.ang.extension.toast
+import java.net.*
+import com.v2ray.ang.service.V2RayServiceManager
+import java.io.IOException
+
+object Utils {
+
+ private val mainStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_MAIN, MMKV.MULTI_PROCESS_MODE) }
+ private val settingsStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_SETTING, MMKV.MULTI_PROCESS_MODE) }
+
+ /**
+ * convert string to editalbe for kotlin
+ *
+ * @param text
+ * @return
+ */
+ fun getEditable(text: String): Editable {
+ return Editable.Factory.getInstance().newEditable(text)
+ }
+
+ /**
+ * find value in array position
+ */
+ fun arrayFind(array: Array, value: String): Int {
+ for (i in array.indices) {
+ if (array[i] == value) {
+ return i
+ }
+ }
+ return -1
+ }
+
+ /**
+ * parseInt
+ */
+ fun parseInt(str: String): Int {
+ return parseInt(str, 0)
+ }
+
+ fun parseInt(str: String?, default: Int): Int {
+ str ?: return default
+ return try {
+ Integer.parseInt(str)
+ } catch (e: Exception) {
+ e.printStackTrace()
+ default
+ }
+ }
+
+ /**
+ * get text from clipboard
+ */
+ fun getClipboard(context: Context): String {
+ return try {
+ val cmb = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
+ cmb.primaryClip?.getItemAt(0)?.text.toString()
+ } catch (e: Exception) {
+ e.printStackTrace()
+ ""
+ }
+ }
+
+ /**
+ * set text to clipboard
+ */
+ fun setClipboard(context: Context, content: String) {
+ try {
+ val cmb = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
+ val clipData = ClipData.newPlainText(null, content)
+ cmb.setPrimaryClip(clipData)
+ } catch (e: Exception) {
+ e.printStackTrace()
+ }
+ }
+
+ /**
+ * base64 decode
+ */
+ fun decode(text: String): String {
+ tryDecodeBase64(text)?.let { return it }
+ if (text.endsWith('=')) {
+ // try again for some loosely formatted base64
+ tryDecodeBase64(text.trimEnd('='))?.let { return it }
+ }
+ return ""
+ }
+
+ fun tryDecodeBase64(text: String): String? {
+ try {
+ return Base64.decode(text, Base64.NO_WRAP).toString(charset("UTF-8"))
+ } catch (e: Exception) {
+ Log.i(ANG_PACKAGE, "Parse base64 standard failed $e")
+ }
+ try {
+ return Base64.decode(text, Base64.NO_WRAP.or(Base64.URL_SAFE)).toString(charset("UTF-8"))
+ } catch (e: Exception) {
+ Log.i(ANG_PACKAGE, "Parse base64 url safe failed $e")
+ }
+ return null
+ }
+
+ /**
+ * base64 encode
+ */
+ fun encode(text: String): String {
+ return try {
+ Base64.encodeToString(text.toByteArray(charset("UTF-8")), Base64.NO_WRAP)
+ } catch (e: Exception) {
+ e.printStackTrace()
+ ""
+ }
+ }
+
+ /**
+ * get remote dns servers from preference
+ */
+ fun getRemoteDnsServers(): List {
+ val remoteDns = settingsStorage?.decodeString(AppConfig.PREF_REMOTE_DNS) ?: AppConfig.DNS_AGENT
+ val ret = remoteDns.split(",").filter { isPureIpAddress(it) || isCoreDNSAddress(it) }
+ if (ret.isEmpty()) {
+ return listOf(AppConfig.DNS_AGENT)
+ }
+ return ret
+ }
+
+ fun getVpnDnsServers(): List {
+ val vpnDns = settingsStorage?.decodeString(AppConfig.PREF_VPN_DNS)
+ ?: settingsStorage?.decodeString(AppConfig.PREF_REMOTE_DNS)
+ ?: AppConfig.DNS_AGENT
+ return vpnDns.split(",").filter { isPureIpAddress(it) }
+ // allow empty, in that case dns will use system default
+ }
+
+ /**
+ * get remote dns servers from preference
+ */
+ fun getDomesticDnsServers(): List {
+ val domesticDns = settingsStorage?.decodeString(AppConfig.PREF_DOMESTIC_DNS) ?: AppConfig.DNS_DIRECT
+ val ret = domesticDns.split(",").filter { isPureIpAddress(it) || isCoreDNSAddress(it) }
+ if (ret.isEmpty()) {
+ return listOf(AppConfig.DNS_DIRECT)
+ }
+ return ret
+ }
+
+ /**
+ * create qrcode using zxing
+ */
+ fun createQRCode(text: String, size: Int = 800): Bitmap? {
+ try {
+ val hints = HashMap()
+ hints[EncodeHintType.CHARACTER_SET] = "utf-8"
+ val bitMatrix = QRCodeWriter().encode(text,
+ BarcodeFormat.QR_CODE, size, size, hints)
+ val pixels = IntArray(size * size)
+ for (y in 0 until size) {
+ for (x in 0 until size) {
+ if (bitMatrix.get(x, y)) {
+ pixels[y * size + x] = 0xff000000.toInt()
+ } else {
+ pixels[y * size + x] = 0xffffffff.toInt()
+ }
+
+ }
+ }
+ val bitmap = Bitmap.createBitmap(size, size,
+ Bitmap.Config.ARGB_8888)
+ bitmap.setPixels(pixels, 0, size, 0, 0, size, size)
+ return bitmap
+ } catch (e: WriterException) {
+ e.printStackTrace()
+ return null
+ }
+ }
+
+ /**
+ * is ip address
+ */
+ fun isIpAddress(value: String): Boolean {
+ try {
+ var addr = value
+ if (addr.isEmpty() || addr.isBlank()) {
+ return false
+ }
+ //CIDR
+ if (addr.indexOf("/") > 0) {
+ val arr = addr.split("/")
+ if (arr.count() == 2 && Integer.parseInt(arr[1]) > 0) {
+ addr = arr[0]
+ }
+ }
+
+ // "::ffff:192.168.173.22"
+ // "[::ffff:192.168.173.22]:80"
+ if (addr.startsWith("::ffff:") && '.' in addr) {
+ addr = addr.drop(7)
+ } else if (addr.startsWith("[::ffff:") && '.' in addr) {
+ addr = addr.drop(8).replace("]", "")
+ }
+
+ // addr = addr.toLowerCase()
+ val octets = addr.split('.').toTypedArray()
+ if (octets.size == 4) {
+ if (octets[3].indexOf(":") > 0) {
+ addr = addr.substring(0, addr.indexOf(":"))
+ }
+ return isIpv4Address(addr)
+ }
+
+ // Ipv6addr [2001:abc::123]:8080
+ return isIpv6Address(addr)
+ } catch (e: Exception) {
+ e.printStackTrace()
+ return false
+ }
+ }
+
+ fun isPureIpAddress(value: String): Boolean {
+ return (isIpv4Address(value) || isIpv6Address(value))
+ }
+
+ fun isIpv4Address(value: String): Boolean {
+ val regV4 = Regex("^([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\\.([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\\.([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\\.([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])$")
+ return regV4.matches(value)
+ }
+
+ fun isIpv6Address(value: String): Boolean {
+ var addr = value
+ if (addr.indexOf("[") == 0 && addr.lastIndexOf("]") > 0) {
+ addr = addr.drop(1)
+ addr = addr.dropLast(addr.count() - addr.lastIndexOf("]"))
+ }
+ val regV6 = Regex("^((?:[0-9A-Fa-f]{1,4}))?((?::[0-9A-Fa-f]{1,4}))*::((?:[0-9A-Fa-f]{1,4}))?((?::[0-9A-Fa-f]{1,4}))*|((?:[0-9A-Fa-f]{1,4}))((?::[0-9A-Fa-f]{1,4})){7}$")
+ return regV6.matches(addr)
+ }
+
+ private fun isCoreDNSAddress(s: String): Boolean {
+ return s.startsWith("https") || s.startsWith("tcp") || s.startsWith("quic")
+ }
+
+ /**
+ * is valid url
+ */
+ fun isValidUrl(value: String?): Boolean {
+ try {
+ if (value != null && Patterns.WEB_URL.matcher(value).matches() || URLUtil.isValidUrl(value)) {
+ return true
+ }
+ } catch (e: WriterException) {
+ e.printStackTrace()
+ return false
+ }
+ return false
+ }
+
+ fun startVServiceFromToggle(context: Context): Boolean {
+ if (mainStorage?.decodeString(MmkvManager.KEY_SELECTED_SERVER).isNullOrEmpty()) {
+ context.toast(R.string.app_tile_first_use)
+ return false
+ }
+ V2RayServiceManager.startV2Ray(context)
+ return true
+ }
+
+ /**
+ * stopVService
+ */
+ fun stopVService(context: Context) {
+ context.toast(R.string.toast_services_stop)
+ MessageUtil.sendMsg2Service(context, AppConfig.MSG_STATE_STOP, "")
+ }
+
+ fun openUri(context: Context, uriString: String) {
+ val uri = Uri.parse(uriString)
+ context.startActivity(Intent(Intent.ACTION_VIEW, uri))
+ }
+
+ /**
+ * uuid
+ */
+ fun getUuid(): String {
+ return try {
+ UUID.randomUUID().toString().replace("-", "")
+ } catch (e: Exception) {
+ e.printStackTrace()
+ ""
+ }
+ }
+
+ fun urlDecode(url: String): String {
+ return try {
+ URLDecoder.decode(URLDecoder.decode(url), "utf-8")
+ } catch (e: Exception) {
+ e.printStackTrace()
+ url
+ }
+ }
+
+ fun urlEncode(url: String): String {
+ return try {
+ URLEncoder.encode(url, "UTF-8")
+ } catch (e: Exception) {
+ e.printStackTrace()
+ url
+ }
+ }
+
+
+ /**
+ * readTextFromAssets
+ */
+ fun readTextFromAssets(context: Context, fileName: String): String {
+ val content = context.assets.open(fileName).bufferedReader().use {
+ it.readText()
+ }
+ return content
+ }
+
+ fun userAssetPath(context: Context?): String {
+ if (context == null)
+ return ""
+ val extDir = context.getExternalFilesDir(AppConfig.DIR_ASSETS)
+ ?: return context.getDir(AppConfig.DIR_ASSETS, 0).absolutePath
+ return extDir.absolutePath
+ }
+
+ fun getUrlContext(url: String, timeout: Int): String {
+ var result: String
+ var conn: HttpURLConnection? = null
+
+ try {
+ conn = URL(url).openConnection() as HttpURLConnection
+ conn.connectTimeout = timeout
+ conn.readTimeout = timeout
+ conn.setRequestProperty("Connection", "close")
+ conn.instanceFollowRedirects = false
+ conn.useCaches = false
+ //val code = conn.responseCode
+ result = conn.inputStream.bufferedReader().readText()
+ } catch (e: Exception) {
+ result = ""
+ } finally {
+ conn?.disconnect()
+ }
+ return result
+ }
+
+ @Throws(IOException::class)
+ fun getUrlContentWithCustomUserAgent(urlStr: String?): String {
+ val url = URL(urlStr)
+ val conn = url.openConnection()
+ conn.setRequestProperty("Connection", "close")
+ conn.setRequestProperty("User-agent", "v2rayNG/${BuildConfig.VERSION_NAME}")
+ url.userInfo?.let {
+ conn.setRequestProperty("Authorization",
+ "Basic ${encode(urlDecode(it))}")
+ }
+ conn.useCaches = false
+ return conn.inputStream.use {
+ it.bufferedReader().readText()
+ }
+ }
+
+ fun getDarkModeStatus(context: Context): Boolean {
+ val mode = context.resources.configuration.uiMode and UI_MODE_NIGHT_MASK
+ return mode == UI_MODE_NIGHT_YES
+ }
+
+ fun getIpv6Address(address: String): String {
+ return if (isIpv6Address(address)) {
+ String.format("[%s]", address)
+ } else {
+ address
+ }
+ }
+
+ fun getLocale(context: Context): Locale =
+ when (settingsStorage?.decodeString(AppConfig.PREF_LANGUAGE) ?: "auto") {
+ "auto" -> getSysLocale()
+ "en" -> Locale("en")
+ "zh-rCN" -> Locale("zh", "CN")
+ "zh-rTW" -> Locale("zh", "TW")
+ "vi" -> Locale("vi")
+ "ru" -> Locale("ru")
+ "fa" -> Locale("fa")
+ else -> getSysLocale()
+ }
+
+ private fun getSysLocale(): Locale = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+ LocaleList.getDefault()[0]
+ } else {
+ Locale.getDefault()
+ }
+
+ fun fixIllegalUrl(str: String): String {
+ return str
+ .replace(" ","%20")
+ .replace("|","%7C")
+ }
+
+ fun removeWhiteSpace(str: String?): String? {
+ return str?.replace(" ", "")
+ }
+
+ fun idnToASCII(str: String): String {
+ val url = URL(str)
+ return URL(url.protocol, IDN.toASCII(url.host, IDN.ALLOW_UNASSIGNED), url.port, url.file)
+ .toExternalForm()
+ }
+}
+
diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/V2rayConfigUtil.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/V2rayConfigUtil.kt
new file mode 100644
index 00000000..f5177aca
--- /dev/null
+++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/V2rayConfigUtil.kt
@@ -0,0 +1,432 @@
+package com.v2ray.ang.util
+
+import android.content.Context
+import android.text.TextUtils
+import android.util.Log
+import com.google.gson.*
+import com.tencent.mmkv.MMKV
+import com.v2ray.ang.AppConfig
+import com.v2ray.ang.AppConfig.ANG_PACKAGE
+import com.v2ray.ang.dto.V2rayConfig
+import com.v2ray.ang.dto.EConfigType
+import com.v2ray.ang.dto.ERoutingMode
+import com.v2ray.ang.dto.V2rayConfig.Companion.DEFAULT_NETWORK
+import com.v2ray.ang.dto.V2rayConfig.Companion.HTTP
+
+object V2rayConfigUtil {
+ private val serverRawStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_SERVER_RAW, MMKV.MULTI_PROCESS_MODE) }
+ private val settingsStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_SETTING, MMKV.MULTI_PROCESS_MODE) }
+
+ data class Result(var status: Boolean, var content: String)
+
+ /**
+ * 生成v2ray的客户端配置文件
+ */
+ fun getV2rayConfig(context: Context, guid: String): Result {
+ try {
+ val config = MmkvManager.decodeServerConfig(guid) ?: return Result(false, "")
+ if (config.configType == EConfigType.CUSTOM) {
+ val raw = serverRawStorage?.decodeString(guid)
+ val customConfig = if (raw.isNullOrBlank()) {
+ config.fullConfig?.toPrettyPrinting() ?: return Result(false, "")
+ } else {
+ raw
+ }
+ Log.d(ANG_PACKAGE, customConfig)
+ return Result(true, customConfig)
+ }
+ val outbound = config.getProxyOutbound() ?: return Result(false, "")
+ val result = getV2rayNonCustomConfig(context, outbound)
+ Log.d(ANG_PACKAGE, result.content)
+ return result
+ } catch (e: Exception) {
+ e.printStackTrace()
+ return Result(false, "")
+ }
+ }
+
+ /**
+ * 生成v2ray的客户端配置文件
+ */
+ private fun getV2rayNonCustomConfig(context: Context, outbound: V2rayConfig.OutboundBean): Result {
+ val result = Result(false, "")
+ //取得默认配置
+ val assets = Utils.readTextFromAssets(context, "v2ray_config.json")
+ if (TextUtils.isEmpty(assets)) {
+ return result
+ }
+
+ //转成Json
+ val v2rayConfig = Gson().fromJson(assets, V2rayConfig::class.java) ?: return result
+
+ v2rayConfig.log.loglevel = settingsStorage?.decodeString(AppConfig.PREF_LOGLEVEL)
+ ?: "warning"
+
+ inbounds(v2rayConfig)
+
+ httpRequestObject(outbound)
+
+ v2rayConfig.outbounds[0] = outbound
+
+ routing(v2rayConfig)
+
+ fakedns(v2rayConfig)
+
+ dns(v2rayConfig)
+
+ if (settingsStorage?.decodeBool(AppConfig.PREF_LOCAL_DNS_ENABLED) == true) {
+ customLocalDns(v2rayConfig)
+ }
+ if (settingsStorage?.decodeBool(AppConfig.PREF_SPEED_ENABLED) != true) {
+ v2rayConfig.stats = null
+ v2rayConfig.policy = null
+ }
+ result.status = true
+ result.content = v2rayConfig.toPrettyPrinting()
+ return result
+ }
+
+ /**
+ *
+ */
+ private fun inbounds(v2rayConfig: V2rayConfig): Boolean {
+ try {
+ val socksPort = Utils.parseInt(settingsStorage?.decodeString(AppConfig.PREF_SOCKS_PORT), AppConfig.PORT_SOCKS.toInt())
+ val httpPort = Utils.parseInt(settingsStorage?.decodeString(AppConfig.PREF_HTTP_PORT), AppConfig.PORT_HTTP.toInt())
+
+ v2rayConfig.inbounds.forEach { curInbound ->
+ if (settingsStorage?.decodeBool(AppConfig.PREF_PROXY_SHARING) != true) {
+ //bind all inbounds to localhost if the user requests
+ curInbound.listen = "127.0.0.1"
+ }
+ }
+ v2rayConfig.inbounds[0].port = socksPort
+ val fakedns = settingsStorage?.decodeBool(AppConfig.PREF_FAKE_DNS_ENABLED)
+ ?: false
+ val sniffAllTlsAndHttp = settingsStorage?.decodeBool(AppConfig.PREF_SNIFFING_ENABLED, true)
+ ?: true
+ v2rayConfig.inbounds[0].sniffing?.enabled = fakedns || sniffAllTlsAndHttp
+ if (!sniffAllTlsAndHttp) {
+ v2rayConfig.inbounds[0].sniffing?.destOverride?.clear()
+ }
+ if (fakedns) {
+ v2rayConfig.inbounds[0].sniffing?.destOverride?.add("fakedns")
+ }
+
+ v2rayConfig.inbounds[1].port = httpPort
+
+// if (httpPort > 0) {
+// val httpCopy = v2rayConfig.inbounds[0].copy()
+// httpCopy.port = httpPort
+// httpCopy.protocol = "http"
+// v2rayConfig.inbounds.add(httpCopy)
+// }
+ } catch (e: Exception) {
+ e.printStackTrace()
+ return false
+ }
+ return true
+ }
+
+ private fun fakedns(v2rayConfig: V2rayConfig) {
+ if (settingsStorage?.decodeBool(AppConfig.PREF_FAKE_DNS_ENABLED) == true) {
+ v2rayConfig.fakedns = listOf(V2rayConfig.FakednsBean())
+ v2rayConfig.outbounds.filter { it.protocol == "freedom" }.forEach {
+ it.settings?.domainStrategy = "UseIP"
+ }
+ }
+ }
+
+ /**
+ * routing
+ */
+ private fun routing(v2rayConfig: V2rayConfig): Boolean {
+ try {
+ routingUserRule(settingsStorage?.decodeString(AppConfig.PREF_V2RAY_ROUTING_AGENT)
+ ?: "", AppConfig.TAG_AGENT, v2rayConfig)
+ routingUserRule(settingsStorage?.decodeString(AppConfig.PREF_V2RAY_ROUTING_DIRECT)
+ ?: "", AppConfig.TAG_DIRECT, v2rayConfig)
+ routingUserRule(settingsStorage?.decodeString(AppConfig.PREF_V2RAY_ROUTING_BLOCKED)
+ ?: "", AppConfig.TAG_BLOCKED, v2rayConfig)
+
+ v2rayConfig.routing.domainStrategy = settingsStorage?.decodeString(AppConfig.PREF_ROUTING_DOMAIN_STRATEGY)
+ ?: "IPIfNonMatch"
+ v2rayConfig.routing.domainMatcher = "mph"
+ val routingMode = settingsStorage?.decodeString(AppConfig.PREF_ROUTING_MODE) ?: ERoutingMode.GLOBAL_PROXY.value
+
+ // Hardcode googleapis.cn
+ val googleapisRoute = V2rayConfig.RoutingBean.RulesBean(
+ type = "field",
+ outboundTag = AppConfig.TAG_AGENT,
+ domain = arrayListOf("domain:googleapis.cn")
+ )
+
+ when (routingMode) {
+ ERoutingMode.BYPASS_LAN.value -> {
+ routingGeo("ip", "private", AppConfig.TAG_DIRECT, v2rayConfig)
+ }
+ ERoutingMode.BYPASS_MAINLAND.value -> {
+ routingGeo("", "cn", AppConfig.TAG_DIRECT, v2rayConfig)
+ v2rayConfig.routing.rules.add(0, googleapisRoute)
+ }
+ ERoutingMode.BYPASS_LAN_MAINLAND.value -> {
+ routingGeo("ip", "private", AppConfig.TAG_DIRECT, v2rayConfig)
+ routingGeo("", "cn", AppConfig.TAG_DIRECT, v2rayConfig)
+ v2rayConfig.routing.rules.add(0, googleapisRoute)
+ }
+ ERoutingMode.GLOBAL_DIRECT.value -> {
+ val globalDirect = V2rayConfig.RoutingBean.RulesBean(
+ type = "field",
+ outboundTag = AppConfig.TAG_DIRECT,
+ port = "0-65535"
+ )
+ v2rayConfig.routing.rules.add(globalDirect)
+ }
+ }
+ } catch (e: Exception) {
+ e.printStackTrace()
+ return false
+ }
+ return true
+ }
+
+ private fun routingGeo(ipOrDomain: String, code: String, tag: String, v2rayConfig: V2rayConfig) {
+ try {
+ if (!TextUtils.isEmpty(code)) {
+ //IP
+ if (ipOrDomain == "ip" || ipOrDomain == "") {
+ val rulesIP = V2rayConfig.RoutingBean.RulesBean()
+ rulesIP.type = "field"
+ rulesIP.outboundTag = tag
+ rulesIP.ip = ArrayList()
+ rulesIP.ip?.add("geoip:$code")
+ v2rayConfig.routing.rules.add(rulesIP)
+ }
+
+ if (ipOrDomain == "domain" || ipOrDomain == "") {
+ //Domain
+ val rulesDomain = V2rayConfig.RoutingBean.RulesBean()
+ rulesDomain.type = "field"
+ rulesDomain.outboundTag = tag
+ rulesDomain.domain = ArrayList()
+ rulesDomain.domain?.add("geosite:$code")
+ v2rayConfig.routing.rules.add(rulesDomain)
+ }
+ }
+ } catch (e: Exception) {
+ e.printStackTrace()
+ }
+ }
+
+ private fun routingUserRule(userRule: String, tag: String, v2rayConfig: V2rayConfig) {
+ try {
+ if (!TextUtils.isEmpty(userRule)) {
+ //Domain
+ val rulesDomain = V2rayConfig.RoutingBean.RulesBean()
+ rulesDomain.type = "field"
+ rulesDomain.outboundTag = tag
+ rulesDomain.domain = ArrayList()
+
+ //IP
+ val rulesIP = V2rayConfig.RoutingBean.RulesBean()
+ rulesIP.type = "field"
+ rulesIP.outboundTag = tag
+ rulesIP.ip = ArrayList()
+
+ userRule.split(",").map { it.trim() }.forEach {
+ if (Utils.isIpAddress(it) || it.startsWith("geoip:")) {
+ rulesIP.ip?.add(it)
+ } else if (it.isNotEmpty())
+// if (Utils.isValidUrl(it)
+// || it.startsWith("geosite:")
+// || it.startsWith("regexp:")
+// || it.startsWith("domain:")
+// || it.startsWith("full:"))
+ {
+ rulesDomain.domain?.add(it)
+ }
+ }
+ if (rulesDomain.domain?.size!! > 0) {
+ v2rayConfig.routing.rules.add(rulesDomain)
+ }
+ if (rulesIP.ip?.size!! > 0) {
+ v2rayConfig.routing.rules.add(rulesIP)
+ }
+ }
+ } catch (e: Exception) {
+ e.printStackTrace()
+ }
+ }
+
+ private fun userRule2Domian(userRule: String): ArrayList {
+ val domain = ArrayList()
+ userRule.split(",").map { it.trim() }.forEach {
+ if (it.startsWith("geosite:") || it.startsWith("domain:")) {
+ domain.add(it)
+ }
+ }
+ return domain
+ }
+
+ /**
+ * Custom Dns
+ */
+ private fun customLocalDns(v2rayConfig: V2rayConfig): Boolean {
+ try {
+ if (settingsStorage?.decodeBool(AppConfig.PREF_FAKE_DNS_ENABLED) == true) {
+ val geositeCn = arrayListOf("geosite:cn")
+ val proxyDomain = userRule2Domian(settingsStorage?.decodeString(AppConfig.PREF_V2RAY_ROUTING_AGENT)
+ ?: "")
+ val directDomain = userRule2Domian(settingsStorage?.decodeString(AppConfig.PREF_V2RAY_ROUTING_DIRECT)
+ ?: "")
+ // fakedns with all domains to make it always top priority
+ v2rayConfig.dns.servers?.add(0,
+ V2rayConfig.DnsBean.ServersBean(address = "fakedns", domains = geositeCn.plus(proxyDomain).plus(directDomain)))
+ }
+
+ // DNS inbound对象
+ val remoteDns = Utils.getRemoteDnsServers()
+ if (v2rayConfig.inbounds.none { e -> e.protocol == "dokodemo-door" && e.tag == "dns-in" }) {
+ val dnsInboundSettings = V2rayConfig.InboundBean.InSettingsBean(
+ address = if (Utils.isPureIpAddress(remoteDns.first())) remoteDns.first() else "1.1.1.1",
+ port = 53,
+ network = "tcp,udp")
+
+ val localDnsPort = Utils.parseInt(settingsStorage?.decodeString(AppConfig.PREF_LOCAL_DNS_PORT), AppConfig.PORT_LOCAL_DNS.toInt())
+ v2rayConfig.inbounds.add(
+ V2rayConfig.InboundBean(
+ tag = "dns-in",
+ port = localDnsPort,
+ listen = "127.0.0.1",
+ protocol = "dokodemo-door",
+ settings = dnsInboundSettings,
+ sniffing = null))
+ }
+
+ // DNS outbound对象
+ if (v2rayConfig.outbounds.none { e -> e.protocol == "dns" && e.tag == "dns-out" }) {
+ v2rayConfig.outbounds.add(
+ V2rayConfig.OutboundBean(
+ protocol = "dns",
+ tag = "dns-out",
+ settings = null,
+ streamSettings = null,
+ mux = null))
+ }
+
+ // DNS routing tag
+ v2rayConfig.routing.rules.add(0, V2rayConfig.RoutingBean.RulesBean(
+ type = "field",
+ inboundTag = arrayListOf("dns-in"),
+ outboundTag = "dns-out",
+ domain = null)
+ )
+ } catch (e: Exception) {
+ e.printStackTrace()
+ return false
+ }
+ return true
+ }
+
+ private fun dns(v2rayConfig: V2rayConfig): Boolean {
+ try {
+ val hosts = mutableMapOf()
+ val servers = ArrayList()
+ val remoteDns = Utils.getRemoteDnsServers()
+ val proxyDomain = userRule2Domian(settingsStorage?.decodeString(AppConfig.PREF_V2RAY_ROUTING_AGENT)
+ ?: "")
+
+ remoteDns.forEach {
+ servers.add(it)
+ }
+ if (proxyDomain.size > 0) {
+ servers.add(V2rayConfig.DnsBean.ServersBean(remoteDns.first(), 53, proxyDomain, null))
+ }
+
+ // domestic DNS
+ val directDomain = userRule2Domian(settingsStorage?.decodeString(AppConfig.PREF_V2RAY_ROUTING_DIRECT)
+ ?: "")
+ val routingMode = settingsStorage?.decodeString(AppConfig.PREF_ROUTING_MODE) ?: ERoutingMode.GLOBAL_PROXY.value
+ if (directDomain.size > 0 || routingMode == ERoutingMode.BYPASS_MAINLAND.value || routingMode == ERoutingMode.BYPASS_LAN_MAINLAND.value) {
+ val domesticDns = Utils.getDomesticDnsServers()
+ val geositeCn = arrayListOf("geosite:cn")
+ val geoipCn = arrayListOf("geoip:cn")
+ if (directDomain.size > 0) {
+ servers.add(V2rayConfig.DnsBean.ServersBean(domesticDns.first(), 53, directDomain, geoipCn))
+ }
+ if (routingMode == ERoutingMode.BYPASS_MAINLAND.value || routingMode == ERoutingMode.BYPASS_LAN_MAINLAND.value) {
+ servers.add(V2rayConfig.DnsBean.ServersBean(domesticDns.first(), 53, geositeCn, geoipCn))
+ }
+ if (Utils.isPureIpAddress(domesticDns.first())) {
+ v2rayConfig.routing.rules.add(0, V2rayConfig.RoutingBean.RulesBean(
+ type = "field",
+ outboundTag = AppConfig.TAG_DIRECT,
+ port = "53",
+ ip = arrayListOf(domesticDns.first()),
+ domain = null)
+ )
+ }
+ }
+
+ val blkDomain = userRule2Domian(settingsStorage?.decodeString(AppConfig.PREF_V2RAY_ROUTING_BLOCKED)
+ ?: "")
+ if (blkDomain.size > 0) {
+ hosts.putAll(blkDomain.map { it to "127.0.0.1" })
+ }
+
+ // hardcode googleapi rule to fix play store problems
+ hosts["domain:googleapis.cn"] = "googleapis.com"
+
+ // DNS dns对象
+ v2rayConfig.dns = V2rayConfig.DnsBean(
+ servers = servers,
+ hosts = hosts)
+
+ // DNS routing
+ if (Utils.isPureIpAddress(remoteDns.first())) {
+ v2rayConfig.routing.rules.add(0, V2rayConfig.RoutingBean.RulesBean(
+ type = "field",
+ outboundTag = AppConfig.TAG_AGENT,
+ port = "53",
+ ip = arrayListOf(remoteDns.first()),
+ domain = null)
+ )
+ }
+ } catch (e: Exception) {
+ e.printStackTrace()
+ return false
+ }
+ return true
+ }
+
+ private fun httpRequestObject(outbound: V2rayConfig.OutboundBean): Boolean {
+ try {
+ if (outbound.streamSettings?.network == DEFAULT_NETWORK
+ && outbound.streamSettings?.tcpSettings?.header?.type == HTTP) {
+ val path = outbound.streamSettings?.tcpSettings?.header?.request?.path
+ val host = outbound.streamSettings?.tcpSettings?.header?.request?.headers?.Host
+
+ val requestString: String by lazy {
+ """{"version":"1.1","method":"GET","headers":{"User-Agent":["Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.143 Safari/537.36","Mozilla/5.0 (iPhone; CPU iPhone OS 10_0_2 like Mac OS X) AppleWebKit/601.1 (KHTML, like Gecko) CriOS/53.0.2785.109 Mobile/14A456 Safari/601.1.46"],"Accept-Encoding":["gzip, deflate"],"Connection":["keep-alive"],"Pragma":"no-cache"}}"""
+ }
+ outbound.streamSettings?.tcpSettings?.header?.request = Gson().fromJson(
+ requestString,
+ V2rayConfig.OutboundBean.StreamSettingsBean.TcpSettingsBean.HeaderBean.RequestBean::class.java
+ )
+ outbound.streamSettings?.tcpSettings?.header?.request?.path =
+ if (path.isNullOrEmpty()) {
+ listOf("/")
+ } else {
+ path
+ }
+ outbound.streamSettings?.tcpSettings?.header?.request?.headers?.Host = host!!
+ }
+
+ } catch (e: Exception) {
+ e.printStackTrace()
+ return false
+ }
+ return true
+ }
+
+}
diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/viewmodel/MainViewModel.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/viewmodel/MainViewModel.kt
new file mode 100644
index 00000000..30673162
--- /dev/null
+++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/viewmodel/MainViewModel.kt
@@ -0,0 +1,235 @@
+package com.v2ray.ang.viewmodel
+
+import android.app.Application
+import android.content.*
+import android.util.Log
+import android.view.LayoutInflater
+import android.widget.ArrayAdapter
+import androidx.appcompat.app.AlertDialog
+import androidx.lifecycle.AndroidViewModel
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.viewModelScope
+import com.google.gson.Gson
+import com.tencent.mmkv.MMKV
+import com.v2ray.ang.AngApplication
+import com.v2ray.ang.AppConfig
+import com.v2ray.ang.AppConfig.ANG_PACKAGE
+import com.v2ray.ang.R
+import com.v2ray.ang.databinding.DialogConfigFilterBinding
+import com.v2ray.ang.dto.*
+import com.v2ray.ang.extension.toast
+import com.v2ray.ang.util.*
+import com.v2ray.ang.util.MmkvManager.KEY_ANG_CONFIGS
+import kotlinx.coroutines.*
+import java.util.*
+
+class MainViewModel(application: Application) : AndroidViewModel(application) {
+ private val mainStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_MAIN, MMKV.MULTI_PROCESS_MODE) }
+ private val serverRawStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_SERVER_RAW, MMKV.MULTI_PROCESS_MODE) }
+
+ var serverList = MmkvManager.decodeServerList()
+ var subscriptionId: String = ""
+ var keywordFilter: String = ""
+ private set
+ val serversCache = mutableListOf()
+ val isRunning by lazy { MutableLiveData() }
+ val updateListAction by lazy { MutableLiveData() }
+ val updateTestResultAction by lazy { MutableLiveData() }
+
+ private val tcpingTestScope by lazy { CoroutineScope(Dispatchers.IO) }
+
+ fun startListenBroadcast() {
+ isRunning.value = false
+ getApplication().registerReceiver(mMsgReceiver, IntentFilter(AppConfig.BROADCAST_ACTION_ACTIVITY))
+ MessageUtil.sendMsg2Service(getApplication(), AppConfig.MSG_REGISTER_CLIENT, "")
+ }
+
+ override fun onCleared() {
+ getApplication().unregisterReceiver(mMsgReceiver)
+ tcpingTestScope.coroutineContext[Job]?.cancelChildren()
+ SpeedtestUtil.closeAllTcpSockets()
+ Log.i(ANG_PACKAGE, "Main ViewModel is cleared")
+ super.onCleared()
+ }
+
+ fun reloadServerList() {
+ serverList = MmkvManager.decodeServerList()
+ updateCache()
+ updateListAction.value = -1
+ }
+
+ fun removeServer(guid: String) {
+ serverList.remove(guid)
+ MmkvManager.removeServer(guid)
+ val index = getPosition(guid)
+ if(index >= 0){
+ serversCache.removeAt(index)
+ }
+ }
+
+ fun appendCustomConfigServer(server: String) {
+ val config = ServerConfig.create(EConfigType.CUSTOM)
+ config.remarks = System.currentTimeMillis().toString()
+ config.subscriptionId = subscriptionId
+ config.fullConfig = Gson().fromJson(server, V2rayConfig::class.java)
+ val key = MmkvManager.encodeServerConfig("", config)
+ serverRawStorage?.encode(key, server)
+ serverList.add(key)
+ serversCache.add(ServersCache(key,config))
+ }
+
+ fun swapServer(fromPosition: Int, toPosition: Int) {
+ Collections.swap(serverList, fromPosition, toPosition)
+ Collections.swap(serversCache, fromPosition, toPosition)
+ mainStorage?.encode(KEY_ANG_CONFIGS, Gson().toJson(serverList))
+ }
+
+ @Synchronized
+ fun updateCache() {
+ serversCache.clear()
+ for (guid in serverList) {
+ val config = MmkvManager.decodeServerConfig(guid) ?: continue
+ if (subscriptionId.isNotEmpty() && subscriptionId != config.subscriptionId) {
+ continue
+ }
+
+ if (keywordFilter.isEmpty() || config.remarks.contains(keywordFilter)) {
+ serversCache.add(ServersCache(guid, config))
+ }
+ }
+ }
+
+ fun testAllTcping() {
+ tcpingTestScope.coroutineContext[Job]?.cancelChildren()
+ SpeedtestUtil.closeAllTcpSockets()
+ MmkvManager.clearAllTestDelayResults()
+ updateListAction.value = -1 // update all
+
+ getApplication().toast(R.string.connection_test_testing)
+ for (item in serversCache) {
+ item.config.getProxyOutbound()?.let { outbound ->
+ val serverAddress = outbound.getServerAddress()
+ val serverPort = outbound.getServerPort()
+ if (serverAddress != null && serverPort != null) {
+ tcpingTestScope.launch {
+ val testResult = SpeedtestUtil.tcping(serverAddress, serverPort)
+ launch(Dispatchers.Main) {
+ MmkvManager.encodeServerTestDelayMillis(item.guid, testResult)
+ updateListAction.value = getPosition(item.guid)
+ }
+ }
+ }
+ }
+ }
+ }
+
+ fun testAllRealPing() {
+ MessageUtil.sendMsg2TestService(getApplication(), AppConfig.MSG_MEASURE_CONFIG_CANCEL, "")
+ MmkvManager.clearAllTestDelayResults()
+ updateListAction.value = -1 // update all
+
+ getApplication().toast(R.string.connection_test_testing)
+ viewModelScope.launch(Dispatchers.Default) { // without Dispatchers.Default viewModelScope will launch in main thread
+ for (item in serversCache) {
+ val config = V2rayConfigUtil.getV2rayConfig(getApplication(), item.guid)
+ if (config.status) {
+ MessageUtil.sendMsg2TestService(getApplication(), AppConfig.MSG_MEASURE_CONFIG, Pair(item.guid, config.content))
+ }
+ }
+ }
+ }
+
+ fun testCurrentServerRealPing() {
+ MessageUtil.sendMsg2Service(getApplication(), AppConfig.MSG_MEASURE_DELAY, "")
+ }
+
+ fun filterConfig(context :Context) {
+ val subscriptions = MmkvManager.decodeSubscriptions()
+ val listId = subscriptions.map { it.first }.toList().toMutableList()
+ val listRemarks = subscriptions.map { it.second.remarks }.toList().toMutableList()
+ listRemarks += context.getString(R.string.filter_config_all)
+ val checkedItem = if (subscriptionId.isNotEmpty()) {
+ listId.indexOf(subscriptionId)
+ } else {
+ listRemarks.count() - 1
+ }
+
+ val ivBinding = DialogConfigFilterBinding.inflate(LayoutInflater.from(context))
+ ivBinding.spSubscriptionId.adapter = ArrayAdapter( context, android.R.layout.simple_spinner_dropdown_item, listRemarks)
+ ivBinding.spSubscriptionId.setSelection(checkedItem)
+ ivBinding.etKeyword.text = Utils.getEditable(keywordFilter)
+ val builder = AlertDialog.Builder(context).setView(ivBinding.root)
+ builder.setTitle(R.string.title_filter_config)
+ builder.setPositiveButton(R.string.tasker_setting_confirm) { dialogInterface: DialogInterface?, _: Int ->
+ try {
+ val position = ivBinding.spSubscriptionId.selectedItemPosition
+ subscriptionId = if (listRemarks.count() - 1 == position) {
+ ""
+ } else {
+ subscriptions[position].first
+ }
+ keywordFilter = ivBinding.etKeyword.text.toString()
+ reloadServerList()
+
+ dialogInterface?.dismiss()
+ } catch (e: Exception) {
+ e.printStackTrace()
+ }
+ }
+ builder.show()
+// AlertDialog.Builder(context)
+// .setSingleChoiceItems(listRemarks.toTypedArray(), checkedItem) { dialog, i ->
+// try {
+// subscriptionId = if (listRemarks.count() - 1 == i) {
+// ""
+// } else {
+// subscriptions[i].first
+// }
+// reloadServerList()
+// dialog.dismiss()
+// } catch (e: Exception) {
+// e.printStackTrace()
+// }
+// }.show()
+ }
+
+ fun getPosition(guid: String) : Int {
+ serversCache.forEachIndexed { index, it ->
+ if (it.guid == guid)
+ return index
+ }
+ return -1
+ }
+
+ private val mMsgReceiver = object : BroadcastReceiver() {
+ override fun onReceive(ctx: Context?, intent: Intent?) {
+ when (intent?.getIntExtra("key", 0)) {
+ AppConfig.MSG_STATE_RUNNING -> {
+ isRunning.value = true
+ }
+ AppConfig.MSG_STATE_NOT_RUNNING -> {
+ isRunning.value = false
+ }
+ AppConfig.MSG_STATE_START_SUCCESS -> {
+ getApplication().toast(R.string.toast_services_success)
+ isRunning.value = true
+ }
+ AppConfig.MSG_STATE_START_FAILURE -> {
+ getApplication().toast(R.string.toast_services_failure)
+ isRunning.value = false
+ }
+ AppConfig.MSG_STATE_STOP_SUCCESS -> {
+ isRunning.value = false
+ }
+ AppConfig.MSG_MEASURE_DELAY_SUCCESS -> {
+ updateTestResultAction.value = intent.getStringExtra("content")
+ }
+ AppConfig.MSG_MEASURE_CONFIG_SUCCESS -> {
+ val resultPair = intent.getSerializableExtra("content") as Pair
+ MmkvManager.encodeServerTestDelayMillis(resultPair.first, resultPair.second)
+ updateListAction.value = getPosition(resultPair.first)
+ }
+ }
+ }
+ }
+}
diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/viewmodel/SettingsViewModel.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/viewmodel/SettingsViewModel.kt
new file mode 100644
index 00000000..2b40cc39
--- /dev/null
+++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/viewmodel/SettingsViewModel.kt
@@ -0,0 +1,64 @@
+package com.v2ray.ang.viewmodel
+
+import android.app.Application
+import android.content.SharedPreferences
+import android.util.Log
+import androidx.lifecycle.AndroidViewModel
+import androidx.preference.PreferenceManager
+import com.tencent.mmkv.MMKV
+import com.v2ray.ang.AppConfig
+import com.v2ray.ang.util.MmkvManager
+
+class SettingsViewModel(application: Application) : AndroidViewModel(application), SharedPreferences.OnSharedPreferenceChangeListener {
+
+ private val settingsStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_SETTING, MMKV.MULTI_PROCESS_MODE) }
+
+ fun startListenPreferenceChange() {
+ PreferenceManager.getDefaultSharedPreferences(getApplication()).registerOnSharedPreferenceChangeListener(this)
+ }
+
+ override fun onCleared() {
+ PreferenceManager.getDefaultSharedPreferences(getApplication()).unregisterOnSharedPreferenceChangeListener(this)
+ Log.i(AppConfig.ANG_PACKAGE, "Settings ViewModel is cleared")
+ super.onCleared()
+ }
+
+ override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String?) {
+ Log.d(AppConfig.ANG_PACKAGE, "Observe settings changed: $key")
+ when(key) {
+ AppConfig.PREF_MODE,
+ AppConfig.PREF_VPN_DNS,
+ AppConfig.PREF_REMOTE_DNS,
+ AppConfig.PREF_DOMESTIC_DNS,
+ AppConfig.PREF_LOCAL_DNS_PORT,
+ AppConfig.PREF_SOCKS_PORT,
+ AppConfig.PREF_HTTP_PORT,
+ AppConfig.PREF_LOGLEVEL,
+ AppConfig.PREF_LANGUAGE,
+ AppConfig.PREF_ROUTING_DOMAIN_STRATEGY,
+ AppConfig.PREF_ROUTING_MODE,
+ AppConfig.PREF_V2RAY_ROUTING_AGENT,
+ AppConfig.PREF_V2RAY_ROUTING_BLOCKED,
+ AppConfig.PREF_V2RAY_ROUTING_DIRECT, -> {
+ settingsStorage?.encode(key, sharedPreferences.getString(key, ""))
+ }
+ AppConfig.PREF_SPEED_ENABLED,
+ AppConfig.PREF_PROXY_SHARING,
+ AppConfig.PREF_LOCAL_DNS_ENABLED,
+ AppConfig.PREF_FAKE_DNS_ENABLED,
+ AppConfig.PREF_ALLOW_INSECURE,
+ AppConfig.PREF_PREFER_IPV6,
+ AppConfig.PREF_PER_APP_PROXY,
+ AppConfig.PREF_BYPASS_APPS,
+ AppConfig.PREF_CONFIRM_REMOVE, -> {
+ settingsStorage?.encode(key, sharedPreferences.getBoolean(key, false))
+ }
+ AppConfig.PREF_SNIFFING_ENABLED -> {
+ settingsStorage?.encode(key, sharedPreferences.getBoolean(key, true))
+ }
+ AppConfig.PREF_PER_APP_PROXY_SET -> {
+ settingsStorage?.encode(key, sharedPreferences.getStringSet(key, setOf()))
+ }
+ }
+ }
+}
diff --git a/V2rayNG/app/src/main/res/anim/fade_in.xml b/V2rayNG/app/src/main/res/anim/fade_in.xml
new file mode 100644
index 00000000..29e04320
--- /dev/null
+++ b/V2rayNG/app/src/main/res/anim/fade_in.xml
@@ -0,0 +1,6 @@
+
+
diff --git a/V2rayNG/app/src/main/res/anim/fade_out.xml b/V2rayNG/app/src/main/res/anim/fade_out.xml
new file mode 100644
index 00000000..2b8bb1cb
--- /dev/null
+++ b/V2rayNG/app/src/main/res/anim/fade_out.xml
@@ -0,0 +1,6 @@
+
+
diff --git a/V2rayNG/app/src/main/res/color/color_highlight_material.xml b/V2rayNG/app/src/main/res/color/color_highlight_material.xml
new file mode 100644
index 00000000..5029d3e9
--- /dev/null
+++ b/V2rayNG/app/src/main/res/color/color_highlight_material.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/V2rayNG/app/src/main/res/drawable-hdpi/ic_stat_direct.png b/V2rayNG/app/src/main/res/drawable-hdpi/ic_stat_direct.png
new file mode 100644
index 00000000..a9fe7cdf
Binary files /dev/null and b/V2rayNG/app/src/main/res/drawable-hdpi/ic_stat_direct.png differ
diff --git a/V2rayNG/app/src/main/res/drawable-hdpi/ic_stat_name.png b/V2rayNG/app/src/main/res/drawable-hdpi/ic_stat_name.png
new file mode 100644
index 00000000..1f0f31fc
Binary files /dev/null and b/V2rayNG/app/src/main/res/drawable-hdpi/ic_stat_name.png differ
diff --git a/V2rayNG/app/src/main/res/drawable-hdpi/ic_stat_proxy.png b/V2rayNG/app/src/main/res/drawable-hdpi/ic_stat_proxy.png
new file mode 100644
index 00000000..048bb16a
Binary files /dev/null and b/V2rayNG/app/src/main/res/drawable-hdpi/ic_stat_proxy.png differ
diff --git a/V2rayNG/app/src/main/res/drawable-mdpi/ic_stat_direct.png b/V2rayNG/app/src/main/res/drawable-mdpi/ic_stat_direct.png
new file mode 100644
index 00000000..0f0d00c6
Binary files /dev/null and b/V2rayNG/app/src/main/res/drawable-mdpi/ic_stat_direct.png differ
diff --git a/V2rayNG/app/src/main/res/drawable-mdpi/ic_stat_name.png b/V2rayNG/app/src/main/res/drawable-mdpi/ic_stat_name.png
new file mode 100644
index 00000000..ac443403
Binary files /dev/null and b/V2rayNG/app/src/main/res/drawable-mdpi/ic_stat_name.png differ
diff --git a/V2rayNG/app/src/main/res/drawable-mdpi/ic_stat_proxy.png b/V2rayNG/app/src/main/res/drawable-mdpi/ic_stat_proxy.png
new file mode 100644
index 00000000..f0a39da4
Binary files /dev/null and b/V2rayNG/app/src/main/res/drawable-mdpi/ic_stat_proxy.png differ
diff --git a/V2rayNG/app/src/main/res/drawable-xhdpi/ic_stat_direct.png b/V2rayNG/app/src/main/res/drawable-xhdpi/ic_stat_direct.png
new file mode 100644
index 00000000..2dcd5d92
Binary files /dev/null and b/V2rayNG/app/src/main/res/drawable-xhdpi/ic_stat_direct.png differ
diff --git a/V2rayNG/app/src/main/res/drawable-xhdpi/ic_stat_name.png b/V2rayNG/app/src/main/res/drawable-xhdpi/ic_stat_name.png
new file mode 100644
index 00000000..7af529f1
Binary files /dev/null and b/V2rayNG/app/src/main/res/drawable-xhdpi/ic_stat_name.png differ
diff --git a/V2rayNG/app/src/main/res/drawable-xhdpi/ic_stat_proxy.png b/V2rayNG/app/src/main/res/drawable-xhdpi/ic_stat_proxy.png
new file mode 100644
index 00000000..36bba560
Binary files /dev/null and b/V2rayNG/app/src/main/res/drawable-xhdpi/ic_stat_proxy.png differ
diff --git a/V2rayNG/app/src/main/res/drawable-xxhdpi/donate.png b/V2rayNG/app/src/main/res/drawable-xxhdpi/donate.png
new file mode 100644
index 00000000..8825c532
Binary files /dev/null and b/V2rayNG/app/src/main/res/drawable-xxhdpi/donate.png differ
diff --git a/V2rayNG/app/src/main/res/drawable-xxhdpi/ic_stat_direct.png b/V2rayNG/app/src/main/res/drawable-xxhdpi/ic_stat_direct.png
new file mode 100644
index 00000000..f8bd77ef
Binary files /dev/null and b/V2rayNG/app/src/main/res/drawable-xxhdpi/ic_stat_direct.png differ
diff --git a/V2rayNG/app/src/main/res/drawable-xxhdpi/ic_stat_name.png b/V2rayNG/app/src/main/res/drawable-xxhdpi/ic_stat_name.png
new file mode 100644
index 00000000..0ac2448f
Binary files /dev/null and b/V2rayNG/app/src/main/res/drawable-xxhdpi/ic_stat_name.png differ
diff --git a/V2rayNG/app/src/main/res/drawable-xxhdpi/ic_stat_proxy.png b/V2rayNG/app/src/main/res/drawable-xxhdpi/ic_stat_proxy.png
new file mode 100644
index 00000000..a10f0fe0
Binary files /dev/null and b/V2rayNG/app/src/main/res/drawable-xxhdpi/ic_stat_proxy.png differ
diff --git a/V2rayNG/app/src/main/res/drawable-xxhdpi/side_nav_bar.xml b/V2rayNG/app/src/main/res/drawable-xxhdpi/side_nav_bar.xml
new file mode 100644
index 00000000..6d81870b
--- /dev/null
+++ b/V2rayNG/app/src/main/res/drawable-xxhdpi/side_nav_bar.xml
@@ -0,0 +1,9 @@
+
+
+
\ No newline at end of file
diff --git a/V2rayNG/app/src/main/res/drawable-xxxhdpi/ic_stat_direct.png b/V2rayNG/app/src/main/res/drawable-xxxhdpi/ic_stat_direct.png
new file mode 100644
index 00000000..406a0c76
Binary files /dev/null and b/V2rayNG/app/src/main/res/drawable-xxxhdpi/ic_stat_direct.png differ
diff --git a/V2rayNG/app/src/main/res/drawable-xxxhdpi/ic_stat_name.png b/V2rayNG/app/src/main/res/drawable-xxxhdpi/ic_stat_name.png
new file mode 100644
index 00000000..dd313da0
Binary files /dev/null and b/V2rayNG/app/src/main/res/drawable-xxxhdpi/ic_stat_name.png differ
diff --git a/V2rayNG/app/src/main/res/drawable-xxxhdpi/ic_stat_proxy.png b/V2rayNG/app/src/main/res/drawable-xxxhdpi/ic_stat_proxy.png
new file mode 100644
index 00000000..daef5970
Binary files /dev/null and b/V2rayNG/app/src/main/res/drawable-xxxhdpi/ic_stat_proxy.png differ
diff --git a/V2rayNG/app/src/main/res/drawable/ic_action_done.xml b/V2rayNG/app/src/main/res/drawable/ic_action_done.xml
new file mode 100644
index 00000000..33a117f6
--- /dev/null
+++ b/V2rayNG/app/src/main/res/drawable/ic_action_done.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/V2rayNG/app/src/main/res/drawable/ic_add_white_24dp.xml b/V2rayNG/app/src/main/res/drawable/ic_add_white_24dp.xml
new file mode 100644
index 00000000..b9b8eca8
--- /dev/null
+++ b/V2rayNG/app/src/main/res/drawable/ic_add_white_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/V2rayNG/app/src/main/res/drawable/ic_attach_money_black_24dp.xml b/V2rayNG/app/src/main/res/drawable/ic_attach_money_black_24dp.xml
new file mode 100644
index 00000000..b520fc98
--- /dev/null
+++ b/V2rayNG/app/src/main/res/drawable/ic_attach_money_black_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/V2rayNG/app/src/main/res/drawable/ic_attach_money_white_24dp.xml b/V2rayNG/app/src/main/res/drawable/ic_attach_money_white_24dp.xml
new file mode 100644
index 00000000..2b65f0c6
--- /dev/null
+++ b/V2rayNG/app/src/main/res/drawable/ic_attach_money_white_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/V2rayNG/app/src/main/res/drawable/ic_close_grey_800_24dp.xml b/V2rayNG/app/src/main/res/drawable/ic_close_grey_800_24dp.xml
new file mode 100644
index 00000000..8a9a5226
--- /dev/null
+++ b/V2rayNG/app/src/main/res/drawable/ic_close_grey_800_24dp.xml
@@ -0,0 +1,10 @@
+
+
+
+
diff --git a/V2rayNG/app/src/main/res/drawable/ic_cloud_download_white_24dp.xml b/V2rayNG/app/src/main/res/drawable/ic_cloud_download_white_24dp.xml
new file mode 100644
index 00000000..171a9b30
--- /dev/null
+++ b/V2rayNG/app/src/main/res/drawable/ic_cloud_download_white_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/V2rayNG/app/src/main/res/drawable/ic_copy_white.xml b/V2rayNG/app/src/main/res/drawable/ic_copy_white.xml
new file mode 100644
index 00000000..e50927b2
--- /dev/null
+++ b/V2rayNG/app/src/main/res/drawable/ic_copy_white.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/V2rayNG/app/src/main/res/drawable/ic_delete_black_24dp.xml b/V2rayNG/app/src/main/res/drawable/ic_delete_black_24dp.xml
new file mode 100644
index 00000000..2f5557af
--- /dev/null
+++ b/V2rayNG/app/src/main/res/drawable/ic_delete_black_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/V2rayNG/app/src/main/res/drawable/ic_delete_white_24dp.xml b/V2rayNG/app/src/main/res/drawable/ic_delete_white_24dp.xml
new file mode 100644
index 00000000..ab38bb6d
--- /dev/null
+++ b/V2rayNG/app/src/main/res/drawable/ic_delete_white_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/V2rayNG/app/src/main/res/drawable/ic_description_black_24dp.xml b/V2rayNG/app/src/main/res/drawable/ic_description_black_24dp.xml
new file mode 100644
index 00000000..38c33351
--- /dev/null
+++ b/V2rayNG/app/src/main/res/drawable/ic_description_black_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/V2rayNG/app/src/main/res/drawable/ic_description_white_24dp.xml b/V2rayNG/app/src/main/res/drawable/ic_description_white_24dp.xml
new file mode 100644
index 00000000..7e0d28e3
--- /dev/null
+++ b/V2rayNG/app/src/main/res/drawable/ic_description_white_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/V2rayNG/app/src/main/res/drawable/ic_edit_black_24dp.xml b/V2rayNG/app/src/main/res/drawable/ic_edit_black_24dp.xml
new file mode 100644
index 00000000..2ab2fb75
--- /dev/null
+++ b/V2rayNG/app/src/main/res/drawable/ic_edit_black_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/V2rayNG/app/src/main/res/drawable/ic_fab_check.xml b/V2rayNG/app/src/main/res/drawable/ic_fab_check.xml
new file mode 100644
index 00000000..54f825f8
--- /dev/null
+++ b/V2rayNG/app/src/main/res/drawable/ic_fab_check.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/V2rayNG/app/src/main/res/drawable/ic_feedback_white_24dp.xml b/V2rayNG/app/src/main/res/drawable/ic_feedback_white_24dp.xml
new file mode 100644
index 00000000..3e08fae2
--- /dev/null
+++ b/V2rayNG/app/src/main/res/drawable/ic_feedback_white_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/V2rayNG/app/src/main/res/drawable/ic_file_white_24dp.xml b/V2rayNG/app/src/main/res/drawable/ic_file_white_24dp.xml
new file mode 100644
index 00000000..f7f9df2e
--- /dev/null
+++ b/V2rayNG/app/src/main/res/drawable/ic_file_white_24dp.xml
@@ -0,0 +1,34 @@
+
+
+
+
+
+
diff --git a/V2rayNG/app/src/main/res/drawable/ic_image_photo.xml b/V2rayNG/app/src/main/res/drawable/ic_image_photo.xml
new file mode 100644
index 00000000..c701a63f
--- /dev/null
+++ b/V2rayNG/app/src/main/res/drawable/ic_image_photo.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/V2rayNG/app/src/main/res/drawable/ic_info_black_24dp.xml b/V2rayNG/app/src/main/res/drawable/ic_info_black_24dp.xml
new file mode 100644
index 00000000..34b8202e
--- /dev/null
+++ b/V2rayNG/app/src/main/res/drawable/ic_info_black_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/V2rayNG/app/src/main/res/drawable/ic_logcat_white_24dp.xml b/V2rayNG/app/src/main/res/drawable/ic_logcat_white_24dp.xml
new file mode 100644
index 00000000..e7d3eb31
--- /dev/null
+++ b/V2rayNG/app/src/main/res/drawable/ic_logcat_white_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/V2rayNG/app/src/main/res/drawable/ic_outline_filter_alt_white_24.xml b/V2rayNG/app/src/main/res/drawable/ic_outline_filter_alt_white_24.xml
new file mode 100644
index 00000000..6cb82eff
--- /dev/null
+++ b/V2rayNG/app/src/main/res/drawable/ic_outline_filter_alt_white_24.xml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/V2rayNG/app/src/main/res/drawable/ic_qu_scan_black_24dp.xml b/V2rayNG/app/src/main/res/drawable/ic_qu_scan_black_24dp.xml
new file mode 100644
index 00000000..e0b32bf4
--- /dev/null
+++ b/V2rayNG/app/src/main/res/drawable/ic_qu_scan_black_24dp.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/V2rayNG/app/src/main/res/drawable/ic_qu_settings_black_24dp.xml b/V2rayNG/app/src/main/res/drawable/ic_qu_settings_black_24dp.xml
new file mode 100644
index 00000000..063171a3
--- /dev/null
+++ b/V2rayNG/app/src/main/res/drawable/ic_qu_settings_black_24dp.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
diff --git a/V2rayNG/app/src/main/res/drawable/ic_qu_switch_black_24dp.xml b/V2rayNG/app/src/main/res/drawable/ic_qu_switch_black_24dp.xml
new file mode 100644
index 00000000..97f8859d
--- /dev/null
+++ b/V2rayNG/app/src/main/res/drawable/ic_qu_switch_black_24dp.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/V2rayNG/app/src/main/res/drawable/ic_rounded_corner_grey.xml b/V2rayNG/app/src/main/res/drawable/ic_rounded_corner_grey.xml
new file mode 100644
index 00000000..50af166d
--- /dev/null
+++ b/V2rayNG/app/src/main/res/drawable/ic_rounded_corner_grey.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/V2rayNG/app/src/main/res/drawable/ic_rounded_corner_theme.xml b/V2rayNG/app/src/main/res/drawable/ic_rounded_corner_theme.xml
new file mode 100644
index 00000000..0463d8bf
--- /dev/null
+++ b/V2rayNG/app/src/main/res/drawable/ic_rounded_corner_theme.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/V2rayNG/app/src/main/res/drawable/ic_save_white_24dp.xml b/V2rayNG/app/src/main/res/drawable/ic_save_white_24dp.xml
new file mode 100644
index 00000000..a7a81a25
--- /dev/null
+++ b/V2rayNG/app/src/main/res/drawable/ic_save_white_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/V2rayNG/app/src/main/res/drawable/ic_scan_black_24dp.xml b/V2rayNG/app/src/main/res/drawable/ic_scan_black_24dp.xml
new file mode 100644
index 00000000..a31063ba
--- /dev/null
+++ b/V2rayNG/app/src/main/res/drawable/ic_scan_black_24dp.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/V2rayNG/app/src/main/res/drawable/ic_select_all_white_24dp.xml b/V2rayNG/app/src/main/res/drawable/ic_select_all_white_24dp.xml
new file mode 100644
index 00000000..a24c01bf
--- /dev/null
+++ b/V2rayNG/app/src/main/res/drawable/ic_select_all_white_24dp.xml
@@ -0,0 +1,10 @@
+
+
+
+
diff --git a/V2rayNG/app/src/main/res/drawable/ic_settings_white_24dp.xml b/V2rayNG/app/src/main/res/drawable/ic_settings_white_24dp.xml
new file mode 100644
index 00000000..ce997a72
--- /dev/null
+++ b/V2rayNG/app/src/main/res/drawable/ic_settings_white_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/V2rayNG/app/src/main/res/drawable/ic_share_black_24dp.xml b/V2rayNG/app/src/main/res/drawable/ic_share_black_24dp.xml
new file mode 100644
index 00000000..e3fe874d
--- /dev/null
+++ b/V2rayNG/app/src/main/res/drawable/ic_share_black_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/V2rayNG/app/src/main/res/drawable/ic_share_white_24dp.xml b/V2rayNG/app/src/main/res/drawable/ic_share_white_24dp.xml
new file mode 100644
index 00000000..90406663
--- /dev/null
+++ b/V2rayNG/app/src/main/res/drawable/ic_share_white_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/V2rayNG/app/src/main/res/drawable/ic_shortcut_background.xml b/V2rayNG/app/src/main/res/drawable/ic_shortcut_background.xml
new file mode 100644
index 00000000..a62b720b
--- /dev/null
+++ b/V2rayNG/app/src/main/res/drawable/ic_shortcut_background.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/V2rayNG/app/src/main/res/drawable/ic_subscriptions_black_24dp.xml b/V2rayNG/app/src/main/res/drawable/ic_subscriptions_black_24dp.xml
new file mode 100644
index 00000000..6f0ed455
--- /dev/null
+++ b/V2rayNG/app/src/main/res/drawable/ic_subscriptions_black_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/V2rayNG/app/src/main/res/drawable/ic_subscriptions_white_24dp.xml b/V2rayNG/app/src/main/res/drawable/ic_subscriptions_white_24dp.xml
new file mode 100644
index 00000000..bc20a83a
--- /dev/null
+++ b/V2rayNG/app/src/main/res/drawable/ic_subscriptions_white_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/V2rayNG/app/src/main/res/drawable/ic_whatshot_black_24dp.xml b/V2rayNG/app/src/main/res/drawable/ic_whatshot_black_24dp.xml
new file mode 100644
index 00000000..1cbc037f
--- /dev/null
+++ b/V2rayNG/app/src/main/res/drawable/ic_whatshot_black_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/V2rayNG/app/src/main/res/drawable/ic_whatshot_white_24dp.xml b/V2rayNG/app/src/main/res/drawable/ic_whatshot_white_24dp.xml
new file mode 100644
index 00000000..ad460f3c
--- /dev/null
+++ b/V2rayNG/app/src/main/res/drawable/ic_whatshot_white_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/V2rayNG/app/src/main/res/drawable/nav_header_bg.png b/V2rayNG/app/src/main/res/drawable/nav_header_bg.png
new file mode 100644
index 00000000..82cb00af
Binary files /dev/null and b/V2rayNG/app/src/main/res/drawable/nav_header_bg.png differ
diff --git a/V2rayNG/app/src/main/res/layout/activity_bypass_list.xml b/V2rayNG/app/src/main/res/layout/activity_bypass_list.xml
new file mode 100644
index 00000000..9baff9a2
--- /dev/null
+++ b/V2rayNG/app/src/main/res/layout/activity_bypass_list.xml
@@ -0,0 +1,92 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/V2rayNG/app/src/main/res/layout/activity_logcat.xml b/V2rayNG/app/src/main/res/layout/activity_logcat.xml
new file mode 100644
index 00000000..06916be1
--- /dev/null
+++ b/V2rayNG/app/src/main/res/layout/activity_logcat.xml
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/V2rayNG/app/src/main/res/layout/activity_main.xml b/V2rayNG/app/src/main/res/layout/activity_main.xml
new file mode 100644
index 00000000..5f3c9e95
--- /dev/null
+++ b/V2rayNG/app/src/main/res/layout/activity_main.xml
@@ -0,0 +1,126 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/V2rayNG/app/src/main/res/layout/activity_none.xml b/V2rayNG/app/src/main/res/layout/activity_none.xml
new file mode 100644
index 00000000..d7681698
--- /dev/null
+++ b/V2rayNG/app/src/main/res/layout/activity_none.xml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/V2rayNG/app/src/main/res/layout/activity_routing_settings.xml b/V2rayNG/app/src/main/res/layout/activity_routing_settings.xml
new file mode 100644
index 00000000..7052cc5f
--- /dev/null
+++ b/V2rayNG/app/src/main/res/layout/activity_routing_settings.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
diff --git a/V2rayNG/app/src/main/res/layout/activity_server_custom_config.xml b/V2rayNG/app/src/main/res/layout/activity_server_custom_config.xml
new file mode 100644
index 00000000..2f1aefc9
--- /dev/null
+++ b/V2rayNG/app/src/main/res/layout/activity_server_custom_config.xml
@@ -0,0 +1,80 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/V2rayNG/app/src/main/res/layout/activity_server_shadowsocks.xml b/V2rayNG/app/src/main/res/layout/activity_server_shadowsocks.xml
new file mode 100644
index 00000000..dba1c142
--- /dev/null
+++ b/V2rayNG/app/src/main/res/layout/activity_server_shadowsocks.xml
@@ -0,0 +1,129 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/V2rayNG/app/src/main/res/layout/activity_server_socks.xml b/V2rayNG/app/src/main/res/layout/activity_server_socks.xml
new file mode 100644
index 00000000..5f34c37c
--- /dev/null
+++ b/V2rayNG/app/src/main/res/layout/activity_server_socks.xml
@@ -0,0 +1,130 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/V2rayNG/app/src/main/res/layout/activity_server_trojan.xml b/V2rayNG/app/src/main/res/layout/activity_server_trojan.xml
new file mode 100644
index 00000000..757a6c78
--- /dev/null
+++ b/V2rayNG/app/src/main/res/layout/activity_server_trojan.xml
@@ -0,0 +1,215 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/V2rayNG/app/src/main/res/layout/activity_server_vless.xml b/V2rayNG/app/src/main/res/layout/activity_server_vless.xml
new file mode 100644
index 00000000..8e70bd02
--- /dev/null
+++ b/V2rayNG/app/src/main/res/layout/activity_server_vless.xml
@@ -0,0 +1,236 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/V2rayNG/app/src/main/res/layout/activity_server_vmess.xml b/V2rayNG/app/src/main/res/layout/activity_server_vmess.xml
new file mode 100644
index 00000000..ebfeb7e1
--- /dev/null
+++ b/V2rayNG/app/src/main/res/layout/activity_server_vmess.xml
@@ -0,0 +1,235 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/V2rayNG/app/src/main/res/layout/activity_settings.xml b/V2rayNG/app/src/main/res/layout/activity_settings.xml
new file mode 100644
index 00000000..f674f2ae
--- /dev/null
+++ b/V2rayNG/app/src/main/res/layout/activity_settings.xml
@@ -0,0 +1,8 @@
+
+
\ No newline at end of file
diff --git a/V2rayNG/app/src/main/res/layout/activity_sub_edit.xml b/V2rayNG/app/src/main/res/layout/activity_sub_edit.xml
new file mode 100644
index 00000000..770f93e4
--- /dev/null
+++ b/V2rayNG/app/src/main/res/layout/activity_sub_edit.xml
@@ -0,0 +1,107 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/V2rayNG/app/src/main/res/layout/activity_sub_setting.xml b/V2rayNG/app/src/main/res/layout/activity_sub_setting.xml
new file mode 100644
index 00000000..bb87f387
--- /dev/null
+++ b/V2rayNG/app/src/main/res/layout/activity_sub_setting.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/V2rayNG/app/src/main/res/layout/activity_tasker.xml b/V2rayNG/app/src/main/res/layout/activity_tasker.xml
new file mode 100644
index 00000000..5c798c37
--- /dev/null
+++ b/V2rayNG/app/src/main/res/layout/activity_tasker.xml
@@ -0,0 +1,48 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/V2rayNG/app/src/main/res/layout/dialog_config_filter.xml b/V2rayNG/app/src/main/res/layout/dialog_config_filter.xml
new file mode 100644
index 00000000..115371df
--- /dev/null
+++ b/V2rayNG/app/src/main/res/layout/dialog_config_filter.xml
@@ -0,0 +1,44 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/V2rayNG/app/src/main/res/layout/fragment_routing_settings.xml b/V2rayNG/app/src/main/res/layout/fragment_routing_settings.xml
new file mode 100644
index 00000000..45f2a305
--- /dev/null
+++ b/V2rayNG/app/src/main/res/layout/fragment_routing_settings.xml
@@ -0,0 +1,39 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/V2rayNG/app/src/main/res/layout/item_qrcode.xml b/V2rayNG/app/src/main/res/layout/item_qrcode.xml
new file mode 100644
index 00000000..3c53162a
--- /dev/null
+++ b/V2rayNG/app/src/main/res/layout/item_qrcode.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
diff --git a/V2rayNG/app/src/main/res/layout/item_recycler_bypass_list.xml b/V2rayNG/app/src/main/res/layout/item_recycler_bypass_list.xml
new file mode 100644
index 00000000..03ad10a7
--- /dev/null
+++ b/V2rayNG/app/src/main/res/layout/item_recycler_bypass_list.xml
@@ -0,0 +1,52 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/V2rayNG/app/src/main/res/layout/item_recycler_footer.xml b/V2rayNG/app/src/main/res/layout/item_recycler_footer.xml
new file mode 100644
index 00000000..268f7690
--- /dev/null
+++ b/V2rayNG/app/src/main/res/layout/item_recycler_footer.xml
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/V2rayNG/app/src/main/res/layout/item_recycler_main.xml b/V2rayNG/app/src/main/res/layout/item_recycler_main.xml
new file mode 100644
index 00000000..acae822a
--- /dev/null
+++ b/V2rayNG/app/src/main/res/layout/item_recycler_main.xml
@@ -0,0 +1,189 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/V2rayNG/app/src/main/res/layout/item_recycler_sub_setting.xml b/V2rayNG/app/src/main/res/layout/item_recycler_sub_setting.xml
new file mode 100644
index 00000000..a4467e27
--- /dev/null
+++ b/V2rayNG/app/src/main/res/layout/item_recycler_sub_setting.xml
@@ -0,0 +1,109 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/V2rayNG/app/src/main/res/layout/item_recycler_user_asset.xml b/V2rayNG/app/src/main/res/layout/item_recycler_user_asset.xml
new file mode 100644
index 00000000..54651771
--- /dev/null
+++ b/V2rayNG/app/src/main/res/layout/item_recycler_user_asset.xml
@@ -0,0 +1,60 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/V2rayNG/app/src/main/res/layout/nav_header.xml b/V2rayNG/app/src/main/res/layout/nav_header.xml
new file mode 100644
index 00000000..2f5d3ed6
--- /dev/null
+++ b/V2rayNG/app/src/main/res/layout/nav_header.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
diff --git a/V2rayNG/app/src/main/res/layout/nav_toolbar.xml b/V2rayNG/app/src/main/res/layout/nav_toolbar.xml
new file mode 100644
index 00000000..262787df
--- /dev/null
+++ b/V2rayNG/app/src/main/res/layout/nav_toolbar.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/V2rayNG/app/src/main/res/layout/nav_view.xml b/V2rayNG/app/src/main/res/layout/nav_view.xml
new file mode 100644
index 00000000..34fd03a3
--- /dev/null
+++ b/V2rayNG/app/src/main/res/layout/nav_view.xml
@@ -0,0 +1,9 @@
+
+
\ No newline at end of file
diff --git a/V2rayNG/app/src/main/res/layout/preference_with_help_link.xml b/V2rayNG/app/src/main/res/layout/preference_with_help_link.xml
new file mode 100644
index 00000000..ec6e6423
--- /dev/null
+++ b/V2rayNG/app/src/main/res/layout/preference_with_help_link.xml
@@ -0,0 +1,11 @@
+
+
diff --git a/V2rayNG/app/src/main/res/layout/tls_layout.xml b/V2rayNG/app/src/main/res/layout/tls_layout.xml
new file mode 100644
index 00000000..2f1f31f7
--- /dev/null
+++ b/V2rayNG/app/src/main/res/layout/tls_layout.xml
@@ -0,0 +1,108 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/V2rayNG/app/src/main/res/layout/widget_switch.xml b/V2rayNG/app/src/main/res/layout/widget_switch.xml
new file mode 100644
index 00000000..81d26fa6
--- /dev/null
+++ b/V2rayNG/app/src/main/res/layout/widget_switch.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
diff --git a/V2rayNG/app/src/main/res/menu/action_server.xml b/V2rayNG/app/src/main/res/menu/action_server.xml
new file mode 100644
index 00000000..f231b056
--- /dev/null
+++ b/V2rayNG/app/src/main/res/menu/action_server.xml
@@ -0,0 +1,14 @@
+
+
\ No newline at end of file
diff --git a/V2rayNG/app/src/main/res/menu/action_sub_setting.xml b/V2rayNG/app/src/main/res/menu/action_sub_setting.xml
new file mode 100644
index 00000000..11cb0c1b
--- /dev/null
+++ b/V2rayNG/app/src/main/res/menu/action_sub_setting.xml
@@ -0,0 +1,19 @@
+
+
\ No newline at end of file
diff --git a/V2rayNG/app/src/main/res/menu/menu_asset.xml b/V2rayNG/app/src/main/res/menu/menu_asset.xml
new file mode 100644
index 00000000..6a59f05a
--- /dev/null
+++ b/V2rayNG/app/src/main/res/menu/menu_asset.xml
@@ -0,0 +1,14 @@
+
+
\ No newline at end of file
diff --git a/V2rayNG/app/src/main/res/menu/menu_bypass_list.xml b/V2rayNG/app/src/main/res/menu/menu_bypass_list.xml
new file mode 100644
index 00000000..a7824754
--- /dev/null
+++ b/V2rayNG/app/src/main/res/menu/menu_bypass_list.xml
@@ -0,0 +1,34 @@
+
+
diff --git a/V2rayNG/app/src/main/res/menu/menu_drawer.xml b/V2rayNG/app/src/main/res/menu/menu_drawer.xml
new file mode 100644
index 00000000..42b81633
--- /dev/null
+++ b/V2rayNG/app/src/main/res/menu/menu_drawer.xml
@@ -0,0 +1,41 @@
+
+
diff --git a/V2rayNG/app/src/main/res/menu/menu_logcat.xml b/V2rayNG/app/src/main/res/menu/menu_logcat.xml
new file mode 100644
index 00000000..e7529dbd
--- /dev/null
+++ b/V2rayNG/app/src/main/res/menu/menu_logcat.xml
@@ -0,0 +1,14 @@
+
+
\ No newline at end of file
diff --git a/V2rayNG/app/src/main/res/menu/menu_main.xml b/V2rayNG/app/src/main/res/menu/menu_main.xml
new file mode 100644
index 00000000..538074cf
--- /dev/null
+++ b/V2rayNG/app/src/main/res/menu/menu_main.xml
@@ -0,0 +1,102 @@
+
+
\ No newline at end of file
diff --git a/V2rayNG/app/src/main/res/menu/menu_routing.xml b/V2rayNG/app/src/main/res/menu/menu_routing.xml
new file mode 100644
index 00000000..3c89797d
--- /dev/null
+++ b/V2rayNG/app/src/main/res/menu/menu_routing.xml
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/V2rayNG/app/src/main/res/menu/menu_scanner.xml b/V2rayNG/app/src/main/res/menu/menu_scanner.xml
new file mode 100644
index 00000000..b986bb5a
--- /dev/null
+++ b/V2rayNG/app/src/main/res/menu/menu_scanner.xml
@@ -0,0 +1,9 @@
+
+
+
+
\ No newline at end of file
diff --git a/V2rayNG/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/V2rayNG/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 00000000..036d09bc
--- /dev/null
+++ b/V2rayNG/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/V2rayNG/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/V2rayNG/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 00000000..036d09bc
--- /dev/null
+++ b/V2rayNG/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/V2rayNG/app/src/main/res/mipmap-hdpi/ic_launcher.png b/V2rayNG/app/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 00000000..cb9b3d3d
Binary files /dev/null and b/V2rayNG/app/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/V2rayNG/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png b/V2rayNG/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png
new file mode 100644
index 00000000..4d38fcce
Binary files /dev/null and b/V2rayNG/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png differ
diff --git a/V2rayNG/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/V2rayNG/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
new file mode 100644
index 00000000..76cd6462
Binary files /dev/null and b/V2rayNG/app/src/main/res/mipmap-hdpi/ic_launcher_round.png differ
diff --git a/V2rayNG/app/src/main/res/mipmap-mdpi/ic_launcher.png b/V2rayNG/app/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 00000000..aa289ac4
Binary files /dev/null and b/V2rayNG/app/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/V2rayNG/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png b/V2rayNG/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png
new file mode 100644
index 00000000..bac8f393
Binary files /dev/null and b/V2rayNG/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png differ
diff --git a/V2rayNG/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/V2rayNG/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
new file mode 100644
index 00000000..010f682b
Binary files /dev/null and b/V2rayNG/app/src/main/res/mipmap-mdpi/ic_launcher_round.png differ
diff --git a/V2rayNG/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/V2rayNG/app/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 00000000..3c04c3b5
Binary files /dev/null and b/V2rayNG/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/V2rayNG/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png b/V2rayNG/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png
new file mode 100644
index 00000000..7da51749
Binary files /dev/null and b/V2rayNG/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png differ
diff --git a/V2rayNG/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/V2rayNG/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
new file mode 100644
index 00000000..e24ac5db
Binary files /dev/null and b/V2rayNG/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ
diff --git a/V2rayNG/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/V2rayNG/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 00000000..1fde9249
Binary files /dev/null and b/V2rayNG/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/V2rayNG/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png b/V2rayNG/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png
new file mode 100644
index 00000000..2bc746a6
Binary files /dev/null and b/V2rayNG/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png differ
diff --git a/V2rayNG/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/V2rayNG/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
new file mode 100644
index 00000000..448a4783
Binary files /dev/null and b/V2rayNG/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ
diff --git a/V2rayNG/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/V2rayNG/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 00000000..647b0a28
Binary files /dev/null and b/V2rayNG/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/V2rayNG/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png b/V2rayNG/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png
new file mode 100644
index 00000000..dc7e3937
Binary files /dev/null and b/V2rayNG/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png differ
diff --git a/V2rayNG/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/V2rayNG/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
new file mode 100644
index 00000000..ad44d89d
Binary files /dev/null and b/V2rayNG/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ
diff --git a/V2rayNG/app/src/main/res/raw/licenses.xml b/V2rayNG/app/src/main/res/raw/licenses.xml
new file mode 100644
index 00000000..06723342
--- /dev/null
+++ b/V2rayNG/app/src/main/res/raw/licenses.xml
@@ -0,0 +1,49 @@
+
+
+ Android Compatibility Library v4
+ http://source.android.com
+ Copyright(C) 2008-2011 The Android Open Source Project
+ Apache Software License 2.0
+
+
+ Android Compatibility Library v7
+ http://source.android.com
+ Copyright(C) 2008-2011 The Android Open Source Project
+ Apache Software License 2.0
+
+
+ Android Design Library
+ http://source.android.com
+ Copyright(C) 2008-2011 The Android Open Source Project
+ Apache Software License 2.0
+
+
+ Google Gson
+ https://github.com/google/gson
+ Copyright 2008-2011 Google Inc.
+ Apache Software License 2.0
+
+
+ kotlin
+ http://kotlinlang.org/
+ Copyright 2010-2016 JetBrains s.r.o.
+ Apache Software License 2.0
+
+
+ Logger
+ https://github.com/orhanobut/logger
+ Copyright 2015 Orhan Obut
+ Apache Software License 2.0
+
+
+ LeakCanary
+ https://github.com/square/leakcanary
+ Copyright 2015 Square, Inc.
+ Apache Software License 2.0
+
+
+ RxPermissions
+ https://github.com/tbruyelle/RxPermissions
+ Apache Software License 2.0
+
+
diff --git a/V2rayNG/app/src/main/res/values-fa/strings.xml b/V2rayNG/app/src/main/res/values-fa/strings.xml
new file mode 100644
index 00000000..47b533aa
--- /dev/null
+++ b/V2rayNG/app/src/main/res/values-fa/strings.xml
@@ -0,0 +1,229 @@
+
+
+ v2rayNG
+ تعویض
+ تعویض
+ برای اولین بار از این ویژگی استفاده میکنید، لطفا از برنامه برای افزودن سرور استفاده کنید
+ باز کردن منو کشویی
+ بستن منو کشویی
+ موفقیت در انتقال داده!
+ انتقال داده انجام نشد!
+
+
+ متوقف
+ قادر به دریافت مجوز نیست
+ برای اطلاعات بیشتر کلیک کنید
+ شروع خدمات
+ توقف خدمات
+ خدمات با موفقیت شروع شد
+ شروع خدمات انجام نشد
+
+
+ پرونده پیکربندی
+ افزودن پیکربندی
+ ذخیره پیکربندی
+ حذف پیکربندی
+ پیکربندی را از QRcode وارد کنید
+ پیکربندی را از کلیپبورد وارد کنید
+ تایپ دستی[Vmess]
+ تایپ دستی[VLESS]
+ تایپ دستی[Shadowsocks]
+ تایپ دستی[Socks]
+ تایپ دستی[Trojan]
+ پیکربندی سفارشی
+ پیکربندی سفارشی را از کلیپبورد وارد کنید
+ پیکربندی سفارشی را به صورت محلی وارد کنید
+ پیکربندی سفارشی را از طریق نشانی اینترنتی وارد کنید
+ نشانی اینترنتی اسکن پیکربندی سفارشی را وارد کنید
+ تایید حذف؟
+ ملاحظات
+ نشانی
+ پورت
+ شناسه
+ alterId
+ امنیت
+ شبکه
+ انتقال
+ نوع head
+ حالت gRPC
+ درخواست میزبان (میزبان/میزبان ws/ میزبان h2)/امنیت QUIC
+ مسیر (مسیر ws/ مسیر h2) کلید QUIC/دانه kcp/نامخدمات gRPC
+ tls
+ uTLS
+ alpn
+ allowInsecure
+ SNI
+ نشانی
+ پورت
+ رمز عبور
+ امنیت
+ رمز عبور (اختیاری)
+ نامکاربری (اختیاری)
+ رمزگذاری
+ جریان
+ موفقیت
+ شکست
+ چیزی نیست
+ پروتکل نادرست
+ رمزگشایی انجام نشد
+ انتخاب پرونده پیکربندی
+ لطفا یک مدیر پرونده نصب کنید.
+ سفارشیسازی پیکربندی
+ پیکربندی معتبر نیست
+ محتوا
+ هیچ دادهای در کلیپبورد وجود ندارد
+ نشانی اینترنتی معتبر نیست
+ اطمینان حاصل کنید که پورت ورودی با تنظیمات مطابقت دارد
+ پیکربندی درست نیست
+ میزبان (SNI) (اختیاری)
+ کپی پرونده انجام نشد، لطفا از مدیر پرونده استفاده کنید
+ افزودن پروندهها
+ دانلود پروندهها
+
+
+ بارگذاری
+ جستجو
+ انتخاب همه
+ کلیدواژهها را وارد کنید
+ حالت Bypass
+ انتخاب خودکار پروکسی برنامه
+ در حال دانلود محتوا
+ خروجی گرفتن در کلیپبورد
+ وارد کردن از کلیپبورد
+
+
+
+ تنظیمات
+ تنظیمات پیشرفته
+ تنظیمات VPN
+ پروکسی هر برنامه
+ عمومی: برنامه بررسی شده پروکسی است، اتصال مستقیم بدون بررسی است. \nحالت bypass: برنامه بررسی شده مستقیما متصل است، پراکسی بررسی نشده است. \nگزینهای برای انتخاب خودکار پروکسی برنامه در منو است
+
+ فعال کردن Mux
+ فعال کردن شاید سرعت بخشیدن به شبکه و تغییر شبکه شاید فلش، بهبود کند
+
+ فعال کردن نمایش سرعت
+ نمایش سرعت فعلی در قسمت آگاهسازی. \nآیکون آگاهسازی بر اساس استفاده تغییر میکند.
+
+ فعال کردن Sniffing
+ دامنه sniff را از بسته امتحان کنید (پیشفرض روشن)
+
+ فعال کردن DNS محلی
+ DNS پردازش شده توسط ماژول DNS هسته (توصیه میشود، در صورت نیاز به دور زدن LAN و نشانی mainland)
+
+ فعال کردن DNS جعلی
+ DNS محلی آدرس IP جعلی را برمیگرداند (سریعتر میباشد، اما ممکن است برای برخی از برنامهها کار نکند)
+
+ IPv6 را ترجیح دهید
+ نشانی و مسیرهای IPv6 را ترجیح دهید
+
+ مسیریابی
+ استراتژی دامنه
+ قوانین از پیش تعریف شده
+ قوانین سفارشی
+
+ DNS از راه دور (اختیاری)
+ DNS
+
+ VPN DNS (فقط IPv4/v6)
+
+ DNS داخلی (اختیاری)
+ DNS
+
+ اجازه اتصالات از طریق LAN
+ دستگاههای دیگر میتوانند از طریق socks/http به پراکسی توسط نشانی آیپی شما متصل شوند، فقط در شبکه مورد اعتماد فعال میشوند تا از اتصال غیرمجاز جلوگیری کنند
+ اتصالات از طریق LAN را مجاز کنید، مطمئن شوید که در یک شبکه قابل اعتماد هستید
+
+ allowInsecure
+ هنگامی که TLS، به طور پیشفرض allowInsecure
+
+ پورت پروکسی SOCKS5
+ پورت پروکسی SOCKS5
+
+ پورت پروکسی HTTP
+ پورت پروکسی HTTP
+
+ پورت DNS محلی
+ پورت DNS محلی
+
+ تایید حذف پرونده پیکربندی
+ آیا برای حذف پرونده پیکربندی نیاز به تایید دوم توسط کاربر است
+
+ بازخورد
+ بهبودهای بازخورد یا اشکالات در گیتهاب
+ عضویت در گروه تلگرام
+ برنامه تلگرام پیدا نشد
+
+ تبلیغات،
+ تبلیغات، برای جزئیات بیشتر کلیک کنید (کمک مالی کنید تا حذف شود)
+
+ سطح گزارشات
+ حالت
+ برای راهنمایی بیشتر روی این متن، کلیک کنید
+ زبان
+
+ گزارشات
+ کپی
+ پاک کردن
+ راهاندازی مجدد خدمات
+ حذف تمام پیکربندی
+ تنظیمات نامعتبر را حذف کنید (ابتدا آزمایش کنید)
+ خروجی گرفتن پیکربندیهای غیرسفارشی در کلیپبورد
+ تنظیمات گروهی اشتراک
+ ملاحظات
+ نشانی اینترنتی اختیاری
+ فعال کردن بهروزرسانی
+ بهروزرسانی اشتراک
+ Tcping همه پیکربندی
+ تاخیر واقعی همه پیکربندی
+ پروندههای دارایی جغرافیا
+ مرتبسازی بر اساس نتایج آزمایش
+ فیلتر پرونده پیکربندی
+ همه گروههای اشتراک
+
+ شروع خدمات
+ تایید
+
+ تنظیمات مسیریابی
+ با کاما (,) از هم جدا شوند، ذخیره کردن هم فراموش نک
+ ذخیره
+ پاک کردن
+ اسکن و جایگزین کنید
+ اسکن و اضافه کنید
+ قوانین مسیریابی پیشفرض را تنظیم کنید
+
+ اتصال را بررسی کنید
+ در حال آزمایش...
+ موفقیت: اتصال HTTP %dms طول کشید
+ اتصال به اینترنت شناسایی نشد: %s
+ اینترنت در دسترس نیست
+ کد خطا: #%d
+ متصل است، برای بررسی اتصال ضربه بزنید
+ متصل نیست
+
+
+ - QRcode
+ - خروجی گرفتن در کلیپبورد
+ - خروجی گرفتن پیکربندی کامل در کلیپبورد
+
+
+
+ - نشانی اینترنتی یا آیپی پروکسی
+ - نشانی اینترنتی یا آیپی مستقیم
+ - نشانی اینترنتی یا آیپی مسدود شده
+
+
+
+ - پروکسی سراسری
+ - دور زدن آدرس LAN و سپس پروکسی
+ - دور زذن آدرس mainland و سپس پروکسی
+ - دور زدن LAN و آدرس mainland و سپس پروکسی
+ - مستقیم سراسری
+
+
+
+ - VPN
+ - فقط پروکسی
+
+
+
diff --git a/V2rayNG/app/src/main/res/values-ru/strings.xml b/V2rayNG/app/src/main/res/values-ru/strings.xml
new file mode 100644
index 00000000..f8b2637d
--- /dev/null
+++ b/V2rayNG/app/src/main/res/values-ru/strings.xml
@@ -0,0 +1,229 @@
+
+
+ v2rayNG
+ Переключить
+ Переключить
+ Первое использование этой функции, пожалуйста, используйте приложение, чтобы добавить сервер
+ Открыть панель навигации
+ Закрыть панель навигации
+ Успешный перенос данных!
+ Перенос данных не выполнен!
+
+
+ Остановить
+ Разрешение не получено
+ Ещё…
+ Запуск служб
+ Остановка служб
+ Службы успешно запущены
+ Сбой при запуске служб
+
+
+ Профиль
+ Добавить профиль
+ Сохранить профиль
+ Удалить профиль
+ Импорт профиля из QR-кода
+ Импорт профиля из буфера обмена
+ Ручной ввод профиля Vmess
+ Ручной ввод профиля VLESS
+ Ручной ввод профиля Shadowsocks
+ Ручной ввод профиля Socks
+ Ручной ввод профиля Trojan
+ Пользовательский профиль
+ Импорт из буфера обмена
+ Импорт с устройства
+ Импорт из URL
+ Импорт сканированием URL
+ Подтверждаете удаление?
+ Описание
+ Адрес
+ Порт
+ ID
+ Альтернативный ID
+ Безопасность
+ Сеть
+ Другие параметры
+ Тип заголовка
+ Режим gRPC
+ Запрос узла (WS/H2) / Шифрование QUIC
+ Путь (WS/H2) / Ключ QUIC / Сид KCP / Сервис gRPC
+ TLS
+ uTLS
+ alpn
+ Разрешать небезопасные
+ SNI
+ Адрес
+ Порт
+ Пароль
+ Безопасность
+ Пароль (необязательно)
+ Пользователь (необязательно)
+ Шифрование
+ Поток
+ Успешно
+ Ошибка
+ Ничего нет
+ Неправильный протокол
+ Невозможно декодировать
+ Выберите файл профиля
+ Установите файловый менеджер
+ Изменить профиль
+ Неправильный профиль
+ Данные
+ В буфере обмена нет данных
+ Неправильный URL
+ Убедитесь, что входящий порт соответствует настройкам
+ Профиль повреждён
+ Узел (SNI) (необязательно)
+ Невозможно скопировать файл, используйте файловый менеджер
+ Добавить файлы
+ Загрузить файлы
+
+
+ Загрузка…
+ Поиск
+ Выбрать все
+ Введите ключевые слова
+ Режим обхода
+ Автовыбор проксируемых приложений
+ Загрузка данных
+ Экспорт в буфер обмена
+ Импорт из буфера обмена
+
+
+
+ Настройки
+ Расширенные настройки
+ Настройки VPN
+ Прокси для выбранных приложений
+ Основной: выделенное приложение соединяется через прокси, не выделенное — напрямую; \n\nРежим обхода: выделенное приложение соединяется напрямую, не выделенное — через прокси.\n\nЕсть возможность автоматического выбора проксируемых приложений в меню.
+
+ Использовать мультиплексирование
+ Включение может ускорить работу и переключение сети
+
+ Отображение скорости
+ Показывать текущую скорость в уведомлении.\nЗначок будет меняться в зависимости от использования.
+
+ Анализ пакетов
+ Использовать анализ пакетов (по умолчанию включено)
+
+ Использовать локальную DNS
+ Обслуживание выполняется DNS-модулем ядра (в настройках маршрутизации рекомендуется выбрать режим «Все, кроме LAN и Китая»)
+
+ Использовать поддельную DNS
+ Локальная DNS возвращает поддельный IP-адрес (быстрее, но может не работать с некоторыми приложениями)
+
+ Предпочитать IPv6
+ Предпочитать IPv6-адреса и маршрутизацию
+
+ Маршрутизация
+ Доменная стратегия
+ Режим маршрутизации
+ Пользовательские правила
+
+ Удалённая DNS (необязательно)
+ DNS
+
+ VPN DNS (только IPv4/v6)
+
+ Внутренняя DNS (необязательно)
+ DNS
+
+ Разрешать подключения из LAN
+ Другие устройства могут подключаться к прокси по вашему IP-адресу через протокол SOCKS/HTTP. Используйте только в надёжной сети, чтобы избежать несанкционированного подключения.
+ Доступ из LAN разрешён, убедитесь, что вы находитесь в надёжной сети
+
+ Разрешать небезопасные
+ Для TLS по умолчанию разрешены небезопасные соединения
+
+ Порт SOCKS5-прокси
+ Порт SOCKS5-прокси
+
+ Порт HTTP-прокси
+ Порт HTTP-прокси
+
+ Локальный порт DNS
+ Локальный порт DNS
+
+ Подтверждение удаления профиля
+ Требовать двойное подтверждение удаления профиля
+
+ Обратная связь
+ Предложить улучшение или сообщить об ошибке на GitHub
+ Присоединиться к группе в Telegram
+ Приложение Telegram не найдено
+
+ Содействие
+ Содействие, нажмите для получения подробной информации (пожертвование может быть удалено)
+
+ Подробность ведения журнала
+ Режим
+ Нажмите для получения дополнительной информации
+ Язык
+
+ Системный журнал
+ Копировать
+ Очистить
+ Перезапуск службы
+ Удалить все профили
+ Удалить сбойные профили (после проверки)
+ Экспорт всех профилей в буфер обмена
+ Группы
+ примечания
+ URL (необязательно)
+ использовать обновление
+ Обновить подписку
+ Проверка доступности профилей
+ Время отклика профилей
+ Файлы георесурсов
+ Сортировка по результатам теста
+ Фильтр профилей
+ Все группы
+
+ Запуск службы
+ Подтвердить
+
+ Настройки маршрутизации
+ Введите требуемые IP/URL через запятую. Не забудьте сохранить изменения.
+ Сохранить
+ Очистить
+ Сканировать и заменить
+ Сканировать и добавить
+ Правила по умолчанию
+
+ Проверить подключение
+ Проверка…
+ Успешно: рукопожатие HTTP заняло %d мс
+ Сбой проверки интернет-соединения: %s
+ Интернет недоступен
+ Код ошибки: #%d
+ Подключено, нажмите для проверки
+ Не подключено
+
+
+ - QR-код
+ - Экспорт в буфер обмена
+ - Экспорт всего профиля в буфер обмена
+
+
+
+ - Проксируемые
+ - Прямые
+ - Блокируемые
+
+
+
+ - Все через прокси
+ - Все, кроме LAN через прокси
+ - Все, кроме Китая через прокси
+ - Все, кроме LAN и Китая через прокси
+ - Все напрямую
+
+
+
+ - VPN
+ - Только прокси
+
+
+
diff --git a/V2rayNG/app/src/main/res/values-sw360dp-v13/values-preference.xml b/V2rayNG/app/src/main/res/values-sw360dp-v13/values-preference.xml
new file mode 100644
index 00000000..99153624
--- /dev/null
+++ b/V2rayNG/app/src/main/res/values-sw360dp-v13/values-preference.xml
@@ -0,0 +1,5 @@
+
+
+ false
+ 0dp
+
\ No newline at end of file
diff --git a/V2rayNG/app/src/main/res/values-vi/strings.xml b/V2rayNG/app/src/main/res/values-vi/strings.xml
new file mode 100644
index 00000000..e9f53f4d
--- /dev/null
+++ b/V2rayNG/app/src/main/res/values-vi/strings.xml
@@ -0,0 +1,228 @@
+
+
+ Kết nối ngay
+ Kết nối ngay
+ Vui lòng thêm một cấu hình vào v2rayNG để sử dụng.
+ Mở menu ứng dụng
+ Đóng menu ứng dụng
+ Đã chuyển dữ liệu!
+ Không thể chuyển dữ liệu!
+
+
+ Ngắt kết nối v2rayNG
+ Vui lòng cấp quyền cần thiết cho v2rayNG. Bạn đã từ chối các quyền cần thiết như Camera hay Bộ nhớ.
+ Nhấn để biết thêm
+ Đang bắt đầu dịch vụ v2rayNG.
+ Đã dừng dịch vụ v2rayNG.
+ Đã bắt đầu dịch vụ v2rayNG.
+ Không thể bắt đầu dịch vụ, hãy thử kiểm tra lại cấu hình hoặc khởi động lại thiết bị.
+
+
+ V2RayNG App :3
+ Thêm cấu hình
+ Lưu cấu hình
+ Xoá cấu hình
+ Nhập cấu hình từ mã QR
+ Nhập cấu hình từ bộ nhớ tạm thời
+ Nhập thủ công [Vmess]
+ Nhập thủ công [VLESS]
+ Nhập thủ công [Shadowsocks]
+ Nhập thủ công [Socks]
+ Nhập thủ công [Trojan]
+ Nâng cao / Cấu hình tùy chỉnh
+ Nhập cấu hình tùy chỉnh từ bộ nhớ tạm thời
+ Nhập cấu hình tùy chỉnh từ Tệp
+ Nhập cấu hình tùy chỉnh từ URL
+ Nhập cấu hình tùy chỉnh quét URL
+ Bạn có muốn xóa cấu hình ?
+ Tên cấu hình
+ Địa chỉ
+ Cổng
+ Địa chỉ ID
+ alterId
+ Bảo mật
+ Kiểu kết nối
+ Nâng cao
+ Kiểu Head
+ Chế độ gRPC
+ Yêu cầu host(host/ws host/h2 host)/Bảo mật QUIC
+ Đường dẫn (ws path/h2 path)/QUIC key/kcp seed/gRPC serviceName
+ tls
+ allowInsecure
+ Địa chỉ SNI
+ Địa chỉ
+ Cổng
+ Mật khẩu
+ Bảo mật
+ Mật khẩu(Bổ sung)
+ Tên người dùng(Bổ sung)
+ Mã hoá
+ flow
+ Đã thực hiện thành công thao tác của bạn, Nếu có gì đó không ổn, hãy thao tác lại.
+ Đã xảy ra lỗi, hãy thử kiểm tra lại hoặc thử lại.
+ Không có gì ở đây
+ Không đúng protocol
+ Không thể decode
+ Vui lòng chọn tệp cấu hình
+ Vui lòng cài đặt trình quản lý tệp để tiếp tục.
+ Cấu hình tùy chỉnh
+ Cấu hình không hợp lệ
+ Nội dung
+ Không có dữ liệu nào trong bộ nhớ tạm thời
+ URL không hợp lệ hoặc không có gì
+ Vui lòng đảm bảo cấu hình tùy chỉnh này không bị lỗi trước khi sử dụng. v2rayNG được Dịch Tiếng Việt bởi CuynuTT😘
+ Cấu hình không hợp lệ
+ Host(SNI)(Bổ sung)
+ Không thể sao chép tệp tin, hãy dùng trình quản lý tệp
+ Thêm tệp
+ Tải xuống tệp tin
+
+
+ Đang tải...
+ Tìm kiếm
+ Chọn tất cả
+ Nhập từ khoá
+ Bỏ qua kết nối VPN
+ Tự động chọn ứng dụng Proxy
+ Đang tải xuống nội dung...
+ Xuất và sao chép
+ Nhập từ bộ nhớ tạm thời
+
+
+
+ Cài đặt
+ Cài đặt nâng cao
+ Cài đặt cho VPN
+ Proxy cho ứng dụng
+ Chung: Ứng dụng đã chọn sẽ kết nối Proxy, Chưa lựa chọn sẽ kết nối trực tiếp; \nBỏ qua kết nối: Ứng dụng được chọn sẽ trực tiếp kết nối, không lựa chọn Proxy. \nLựa chọn để tự động chọn ứng dụng Proxy trong Menu.
+
+ Cho phép Mux
+ Bật lên có thể làm tăng tốc độ mạng và chuyển mạng nhanh hơn.
+
+ Cho phép hiển thị tốc độ mạng
+ Hiển thị tốc độ mạng hiện tại trên thanh thông báo.\nBiểu tượng trên thanh trạng thái có thể thay đổi tùy vào mức sử dụng.
+
+ Cho phép Sniffing
+ Thử chuyển kết nối hiện tại của bạn qua trung gian để trung gian xử lý kết nối về lại cho bạn (Mặc định là bật, hãy tắt nó nếu kết nối không ổn định.)
+
+ Cho phép DNS cục bộ
+ DNS được xử lý bởi mô đun của lõi DNS.
+(Khuyến cáo, nếu cần lộ trình Bẻ khoá LAN và
+ địa chỉ mainland)
+
+ Cho phép DNS giả
+ DNS cục bộ trả về địa chỉ IP giả (Nhanh hơn, nhưng có thể không hoạt động với một số ứng dụng)
+
+ Ưu tiên IPv6
+ Ưu tiên sử dụng địa chỉ IPv6 cho kết nối và lộ trình.
+
+ Lộ trình
+ Tùy chọn tên miền
+ Tùy chỉnh quy tắc lộ trình
+ Tùy chỉnh lộ trình
+
+ Điều khiển DNS (Bổ sung)
+ DNS
+
+ VPN DNS (Chỉ IPv4/v6)
+
+ Domestic DNS (Bổ sung)
+ DNS
+
+ Cho phép kết nối từ mạng LAN
+ Các thiết bị khác có thể kết nối đến proxy bởi địa chỉ IP thông qua socks/http, Chỉ bật khi bạn tin tưởng kết nối để tránh kết nối lạ.
+ Cho phép kết nối từ mạng LAN, Đảm bảo rằng bạn tin tưởng kết nối hiện tại.
+
+ Cho phép đặt lại allowInsecure
+ Khi kết nối TLS, đặt cài đặt allowInsecure thành mặc định
+
+ Cổng Proxy SOCKS5
+ Cổng Proxy SOCKS5
+
+ Cổng Proxy HTTP
+ Cổng Proxy HTTP
+
+ Cổng DNS cục bộ
+ Cổng DNS cục bộ
+
+ Hiển thị thông báo xác nhận xoá cấu hình
+ Hiển thị thông báo xác nhận xoá cấu hình khi bạn xoá một cấu hình.
+
+ Phản hồi lỗi
+ Phản hồi cải tiến hoặc bug lên GitHub
+ Tham gia nhóm Telegram
+ Không tìm thấy ứng dụng Telegram
+
+ Quảng cáo Server
+ Quảng cáo,nhấn để biết thêm(Ủng hộ có thể được gỡ bỏ)
+
+ Mức độ nhật ký
+ Chế độ kết nối
+ Nhấn vào đây nếu bạn cần trợ giúp
+ Ngôn ngữ ứng dụng
+
+ Nhật ký hoạt động
+ Sao chép nhật ký
+ Xoá nhật ký
+ Kết nối lại v2rayNG
+ Xoá tất cả cấu hình
+ Xoá cấu hình lỗi (Kiểm tra trước)
+ Xuất và sao chép tất cả cấu hình
+ Các gói đăng ký
+ Tên các gói đăng ký
+ URL gói đăng ký
+ Sử dụng gói đăng ký này
+ Cập nhật các gói đăng ký
+ Ping tất cả máy chủ
+ Kiểm tra máy chủ
+ Tệp Geo assets
+ Sắp xếp lại theo lần kiểm tra cuối cùng
+ Lọc cấu hình theo các gói đăng ký
+ Hiển thị tất cả các gói đăng ký
+
+ Bắt đầu dịch vụ
+ Xác nhận
+
+ Cài đặt lộ trình
+ Được phân cách bằng dấu chấm phẩy(,),Hãy nhớ nó để lưu lại.
+ Lưu lại
+ Xoá
+ Dò và thay thế
+ Dò và nối
+ Đặt luật lệ lộ trình mặc định
+
+ Kiểm tra kết nối
+ Đang kiểm tra kết nối mạng…
+ Đã kiểm tra kết nối mạng thành công, Ping hiện tại là %d
+ Lỗi kết nối mạng hãy thử đổi cấu hình hoặc kiểm tra lại. Mã lỗi: %s
+ Không có kết nối mạng
+ Mã lỗi: #%d
+ Đã kết nối, hãy nhấn vào đây để kiểm tra kết nối mạng.
+ Chưa kết nối, hãy thêm một cấu hình để kết nối. Đừng để bị lừa đảo bởi cấu hình mất tiền,Dịch TV bởi CuynuTT😘
+
+
+ - Xuất ra mã QR (Chụp màn hình để lưu)
+ - Sao chép cấu hình này
+ - Sao chép thành cấu hình tùy chỉnh
+
+
+
+ - proxy URL hoặc IP
+ - direct URL hoặc IP
+ - URL đã chặn hoặc IP
+
+
+
+ - Proxy Global
+ - Bẻ khoá địa chỉ LAN rồi proxy
+ - Bẻ khoá địa chỉ mainland rồi proxy
+ - Bẻ khoá LAN và địa chỉ mainland rồi proxy
+ - Trực tiếp Global
+
+
+
+ - Chế độ VPN
+ - Chế độ Proxy
+
+
+
diff --git a/V2rayNG/app/src/main/res/values-zh-rCN/strings.xml b/V2rayNG/app/src/main/res/values-zh-rCN/strings.xml
new file mode 100644
index 00000000..e6e395c8
--- /dev/null
+++ b/V2rayNG/app/src/main/res/values-zh-rCN/strings.xml
@@ -0,0 +1,226 @@
+
+
+ 开关
+ 开关
+ 初次使用此功能请先用APP添加配置
+ Open navigation drawer
+ Close navigation drawer
+ 数据迁移成功!
+ 数据迁移失败啦!
+
+
+ 停止
+ 无法取得权限
+ 点击了解更多
+ 启动服务中
+ 关闭中
+ 启动服务成功
+ 启动服务失败
+
+
+ 配置文件
+ 添加配置
+ 保存配置
+ 删除配置
+ 扫描二维码
+ 从剪贴板导入
+ 手动输入[Vmess]
+ 手动输入[VLESS]
+ 手动输入[Shadowsocks]
+ 手动输入[Socks]
+ 手动输入[Trojan]
+ 自定义配置
+ 从剪贴板导入自定义配置
+ 从本地导入自定义配置
+ 剪贴板URL导入自定义配置
+ 扫描URL导入自定义配置
+ 确认删除?
+ 别名(remarks)
+ 地址(address)
+ 端口(port)
+ 用户ID(id)
+ 额外ID(alterId)
+ 加密方式(security)
+ 传输协议(network)
+ 底层传输方式(transport)
+ 伪装类型(type)
+ gRPC 传输模式 (mode)
+ 伪装域名(host)(host/ws host/h2 host)/QUIC 加密方式
+ path(ws path/h2 path)/QUIC 加密密钥/kcp seed/gRPC serviceName
+ 传输层安全(tls)
+ 跳过证书验证(allowInsecure)
+ SNI
+ 服务器地址
+ 服务器端口
+ 密码
+ 加密方式
+ 密码(可选)
+ 用户名(可选)
+ 加密(encryption)
+ 流控(flow)
+ 成功
+ 失败
+ 没有数据
+ 不正确的协议
+ 解码失败
+ 选择一个配置文件
+ 请安装一个文件管理器
+ 自定义配置
+ 无效的配置文件
+ 内容
+ 剪贴板中没有数据
+ 无效的网址
+ 确保inbounds port和设置中的一致
+ 配置格式错误
+ Host(SNI)(可选)
+ 失败, 请使用文件管理器
+ 添加文件
+ 下载文件
+
+
+
+ 正在加载
+ 搜索
+ 全选
+ 输入关键字
+ 绕行模式
+ 自动选中需代理应用
+ 正在下载内容
+ 导出至剪贴板
+ 从剪贴板导入
+
+
+ 设置
+ 进阶设置
+ VPN 设置
+ 分应用代理
+ 常规:勾选的App被代理,未勾选的直连;\n绕行模式:勾选的App直连,未勾选的被代理.\n不明白者在菜单中选择自动选中需代理应用
+
+ 启用Mux多路复用
+ 开启可能会加速,关闭可能会减少断流
+
+ 启用速度显示
+ 在通知中显示当前速度\n小图标显示流量的路由情况
+
+ 启用流量探测
+ 从流量中探测域名 (默认启用)
+
+ 启用本地DNS
+ DNS 请求导入 core 由 DNS 模块处理(推荐启用 如果需要路由绕过局域网及大陆地址)
+
+ 启用虚拟DNS
+ 本地返回虚构解析结果 (减低延时 但个别应用可能无法使用)
+
+ IPv6优先
+ App优先使用IPv6地址连接服务器,同时开启VPN的IPv6路由
+
+ 路由设置
+ 域名策略
+ 预定义规则
+ 自定义规则
+
+ 远程DNS (可选)
+ DNS
+
+ VPN DNS (仅支持 IPv4/v6)
+
+ 境内DNS (可选)
+ DNS
+
+ 允许来自局域网的连接
+ 其他设备可以使用socks/http协议通过您的IP地址连接到代理,仅在受信任的网络中启用以避免未经授权的连接
+ 允许来自局域网的连接,请确保处于受信网络
+
+ 跳过证书验证(allowInsecure)
+ 传输层安全选tls时,默认跳过证书验证(allowInsecure)
+
+ SOCKS5代理端口
+ SOCKS5代理端口
+
+ HTTP代理端口
+ HTTP代理端口
+
+ 本地DNS端口
+ 本地DNS端口
+
+ 删除配置文件确认
+ 删除配置文件是否需要用户二次确认
+
+ 反馈
+ 反馈改进或漏洞至 GitHub
+ 加入Telegram Group
+ 未找到Telegram app
+
+ 推广
+ 一些推广,点击查看详情(捐赠可去除)
+
+ 日志级别
+ 模式
+ 点此查看更多帮助
+ 语言
+
+ Logcat
+ 复制
+ 清除
+ 服务重启
+ 删除全部配置
+ 删除无效配置(先测试)
+ 导出全部(非自定义)配置至剪贴板
+ 订阅分组设置
+ 备注
+ 可选地址(url)
+ 启用更新
+ 更新订阅
+ 测试全部配置Tcping
+ 测试全部配置真连接
+ Geo 资源文件
+ 按测试结果排序
+ 过滤配置文件
+ 所有订阅分组
+
+ 启动服务
+ 确定
+
+ 路由设置
+ 用逗号(,)隔开,可以一行多个,记得保存
+ 保存
+ 清空
+ 扫描并替换
+ 扫描并追加
+ 设置默认路由规则
+
+ "检查网络连接"
+ "测试中…"
+ "连接成功:延时 %d 毫秒"
+ "失败:%s"
+ "无互联网连接"
+ "状态码无效(#%d)"
+ "已连接,点击测试连接"
+ "未连接"
+
+
+ - 二维码
+ - 导出至剪贴板
+ - 导出完整配置至剪贴板
+
+ share_method
+
+ - 代理的网址或IP
+ - 直连的网址或IP
+ - 阻止的网址或IP
+
+
+
+ - 全局代理
+ - 绕过局域网地址而后代理
+ - 绕过大陆地址而后代理
+ - 绕过局域网及大陆地址而后代理
+ - 全局直连
+
+
+
+ - VPN
+ - 仅代理
+
+
+
diff --git a/V2rayNG/app/src/main/res/values-zh-rTW/strings.xml b/V2rayNG/app/src/main/res/values-zh-rTW/strings.xml
new file mode 100644
index 00000000..4b7234b5
--- /dev/null
+++ b/V2rayNG/app/src/main/res/values-zh-rTW/strings.xml
@@ -0,0 +1,226 @@
+
+
+ 開關
+ 開關
+ 首次使用此功能,請使用此應用程式新增伺服器
+ 開啟導覽匣
+ 關閉導覽匣
+ 資料遷移成功!
+ 資料遷移失敗!
+
+
+ 停止
+ 無法取得此權限
+ 瞭解更多
+ 啟動服務
+ 停止服務
+ 啟動服務成功
+ 啟動服務失敗
+
+
+ 組態檔案
+ 新增組態
+ 儲存組態
+ 刪除組態
+ 從 QR Code 匯入組態
+ 從剪貼簿匯入組態
+ 手動鍵入 [Vmess]
+ 手動鍵入 [VLESS]
+ 手動鍵入 [Shadowsocks]
+ 手動鍵入 [Socks]
+ 手動鍵入 [Trojan]
+ 自訂組態
+ 從剪貼簿匯入自訂組態
+ 從本地匯入自訂組態
+ 從 URL 匯入自訂組態
+ 掃描 URL 匯入自訂組態
+ 確定刪除?
+ 備註
+ 位址
+ 埠
+ 使用者 ID
+ alterId
+ 安全性
+ 網路
+ 底層傳輸方式 (transport)
+ 標頭類型
+ gRPC 傳輸模式 (mode)
+ 要求主機 (host)(host/ws host/h2 host)/QUIC 加密方式
+ path(ws path/h2 path)/QUIC 加密金鑰/kcp seed/gRPC serviceName
+ 傳輸層安全 (tls)
+ 跳過憑證驗證 (allowInsecure)
+ SNI
+ 伺服器位址
+ 伺服器埠
+ 密碼
+ 加密方式
+ 密碼 (可選)
+ 使用者名稱 (可選)
+ 加密 (encryption)
+ 流程 (flow)
+ 成功
+ 失敗
+ 無資料
+ 通訊協定不正確
+ 解碼失敗
+ 選取一個組態檔
+ 請安裝檔案總管。
+ 自訂組態
+ 無效組態
+ 內容
+ 剪貼簿內無資料
+ URL 無效
+ 確保 inbounds port 和設定中的一致
+ 組態格式不正確
+ Host(SNI)(可選)
+ 失敗,請使用檔案總管
+ 新增檔案
+ 下載檔案
+
+
+ 載入
+ 搜尋
+ 全選
+ 輸入關鍵字
+ 略過模式
+ 自動選中需 Proxy 應用
+ 正在下載內容
+ 匯出至剪貼簿
+ 從剪貼簿匯入
+
+
+
+ 設定
+ 進階
+ VPN 設定
+ Proxy 個別應用程式
+ 常規:勾選的 App 啟用 Proxy,未勾選的直接連線;\n繞行模式:勾選的 App 直接連線,未勾選的啟用 Proxy。\n可在選單中選擇自動選中需 Proxy 應用
+
+ 啟用 Mux
+ 啟用或許會加快網路速度,切換或許會閃爍
+
+ 啟用速度顯示
+ 在通知中顯示當前速度\n小圖示顯示流量的轉送狀況
+
+ 啟用流量監聽
+ 從流量中監聽網域 (預設啟用)
+
+ 啟用本機 DNS
+ DNS 請求匯入 core 由 DNS 模塊處理 (建議啟用,如果需要轉送略過區域網路及中國大陸)
+
+ 啟用假 DNS
+ 本機退回假解析結果 (減低延時,但個別應用可能無法使用)
+
+ IPv6 偏好
+ App 優先使用 IPv6 位址連線伺服器,同时開啟 VPN 的 IPv6 路由
+
+ 轉送設定
+ 網域策略
+ 轉送模式
+ 自訂轉送
+
+ 遠端 DNS (可選)
+ DNS
+
+ VPN DNS (僅支援 IPv4/v6)
+
+ 國內 DNS (可選)
+ DNS
+
+ 允許來自區域網路的連線
+ 其他裝置可以使用 socks/http 協定透過您的 IP 位址連線到 Proxy,僅在受信任的網路中啟用以避免未經授權的連線
+ 允許來自區域網路的連線,請確保處於受信網路
+
+ 跳過憑證驗證 (allowInsecure)
+ 傳輸層安全選 tls 時,預設跳過憑證驗證 (allowInsecure)
+
+ SOCKS5 Proxy 埠
+ SOCKS5 Proxy 埠
+
+ HTTP Proxy 埠
+ HTTP Proxy 埠
+
+ 本機 DNS 埠
+ 本機 DNS 埠
+
+ 刪除配置文件確認
+ 刪除配置文件是否需要用戶二次確認
+
+ 意見回饋
+ 前往 GitHub 回報錯誤
+ 加入 Telegram 群組
+ 未找到 Telegram 應用程式
+
+ 推廣
+ 一些推廣,輕觸以檢視 (捐贈可去除)
+
+ 記錄層級
+ 模式
+ 輕觸以檢視說明
+ 語言
+
+ Logcat
+ 複製
+ 清除
+ 服務重啟
+ 刪除全部組態
+ 刪除無效組態 (先偵測)
+ 匯出全部 (非自訂) 組態至剪貼簿
+ 訂閱分組設定
+ 備註
+ Optional URL
+ 啟用更新
+ 更新訂閱
+ 偵測所有組態 Tcping
+ 偵測所有組態真延遲
+ Geo 資源檔案
+ 依偵測結果排序
+ 過濾組態
+ 所有訂閱分組
+
+ 啟動服務
+ 確定
+
+ 轉送設定
+ 以半形逗號「,」分隔,並手動儲存
+ 儲存
+ 清除
+ 掃描並取代
+ 掃描並附加
+ 設定預設轉送規則
+
+ "測試連線能力"
+ "測試中……"
+ "成功:%d ms延遲"
+ "測試網際網路連線失敗:%s"
+ "無法使用網際網路"
+ "錯誤碼:(#%d)"
+ "已連線,輕觸以檢查連線能力"
+ "未連線"
+
+
+ - QR Code
+ - 匯出至剪貼簿
+ - 匯出完整組態至剪貼簿
+
+
+
+ - Proxy URL 或 IP
+ - 直接連線 URL 或 IP
+ - 已封鎖的 URL 或 IP
+
+
+
+ - 全域 Proxy
+ - 略過區域網路的 Proxy
+ - 略過中國大陸的 Proxy
+ - 略過區域網路及中國大陸的 Proxy
+ - 直接連線
+
+
+
+ - VPN
+ - 僅 Proxy
+
+
+
diff --git a/V2rayNG/app/src/main/res/values/arrays.xml b/V2rayNG/app/src/main/res/values/arrays.xml
new file mode 100644
index 00000000..0fdb316f
--- /dev/null
+++ b/V2rayNG/app/src/main/res/values/arrays.xml
@@ -0,0 +1,180 @@
+
+
+
+ - chacha20-poly1305
+ - aes-128-gcm
+ - auto
+ - none
+ - zero
+
+
+ - aes-256-gcm
+ - aes-128-gcm
+ - chacha20-poly1305
+ - chacha20-ietf-poly1305
+ - xchacha20-poly1305
+ - xchacha20-ietf-poly1305
+ - none
+ - plain
+ - 2022-blake3-aes-128-gcm
+ - 2022-blake3-aes-256-gcm
+ - 2022-blake3-chacha20-poly1305
+
+
+
+ - tcp
+ - kcp
+ - ws
+ - h2
+ - quic
+ - grpc
+
+
+
+ - none
+ - http
+
+
+
+ - none
+ - srtp
+ - utp
+ - wechat-video
+ - dtls
+ - wireguard
+
+
+
+ - gun
+ - multi
+
+
+
+
+
+ - tls
+
+
+
+ - tls
+ - xtls
+
+
+
+
+ - chrome
+ - firefox
+ - safari
+ - randomized
+
+
+
+
+ - h2
+ - http/1.1
+ - h2,http/1.1
+
+
+
+
+ - true
+ - false
+
+
+
+ - 0
+ - 1
+ - 2
+ - 3
+ - 4
+
+
+
+ - AsIs
+ - IPIfNonMatch
+ - IPOnDemand
+
+
+
+ - debug
+ - info
+ - warning
+ - error
+ - none
+
+
+
+ - VPN
+ - Proxy only
+
+
+
+
+ - xtls-rprx-origin
+ - xtls-rprx-origin-udp443
+ - xtls-rprx-direct
+ - xtls-rprx-direct-udp443
+ - xtls-rprx-splice
+ - xtls-rprx-splice-udp443
+ - xtls-rprx-vision
+ - xtls-rprx-vision-udp443
+
+
+
+
+
+ - 0.0.0.0/5
+ - 8.0.0.0/7
+ - 11.0.0.0/8
+ - 12.0.0.0/6
+ - 16.0.0.0/4
+ - 32.0.0.0/3
+ - 64.0.0.0/2
+ - 128.0.0.0/3
+ - 160.0.0.0/5
+ - 168.0.0.0/6
+ - 172.0.0.0/12
+ - 172.32.0.0/11
+ - 172.64.0.0/10
+ - 172.128.0.0/9
+ - 173.0.0.0/8
+ - 174.0.0.0/7
+ - 176.0.0.0/4
+ - 192.0.0.0/9
+ - 192.128.0.0/11
+ - 192.160.0.0/13
+ - 192.169.0.0/16
+ - 192.170.0.0/15
+ - 192.172.0.0/14
+ - 192.176.0.0/12
+ - 192.192.0.0/10
+ - 193.0.0.0/8
+ - 194.0.0.0/7
+ - 196.0.0.0/6
+ - 200.0.0.0/5
+ - 208.0.0.0/4
+ - 240.0.0.0/4
+
+
+
+
+ - auto
+ - English
+ - 中文简体
+ - 中文繁體
+ - Tiếng Việt
+ - Русский
+ - فارسی
+
+
+
+
+ - auto
+ - en
+ - zh-rCN
+ - zh-rTW
+ - vi
+ - ru
+ - fa
+
+
diff --git a/V2rayNG/app/src/main/res/values/attrs.xml b/V2rayNG/app/src/main/res/values/attrs.xml
new file mode 100644
index 00000000..1f970e54
--- /dev/null
+++ b/V2rayNG/app/src/main/res/values/attrs.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/V2rayNG/app/src/main/res/values/colors.xml b/V2rayNG/app/src/main/res/values/colors.xml
new file mode 100644
index 00000000..a754f5d6
--- /dev/null
+++ b/V2rayNG/app/src/main/res/values/colors.xml
@@ -0,0 +1,24 @@
+
+
+ #009966
+ #FF0099
+ #247BA0
+ #009966
+ #999999
+ #CFD8DC
+ #212121
+
+
+
+ #2B2B2B
+ #161616
+ #D81B60
+ #FFFFFF
+ #000000
+
+ #2B2B2B
+ #161616
+ #12976F
+ #252525
+ #CCCCCC
+
diff --git a/V2rayNG/app/src/main/res/values/dimens.xml b/V2rayNG/app/src/main/res/values/dimens.xml
new file mode 100644
index 00000000..d082409a
--- /dev/null
+++ b/V2rayNG/app/src/main/res/values/dimens.xml
@@ -0,0 +1,16 @@
+
+
+ 50dp
+ 16dp
+ 16dp
+ 8dp
+ 50dp
+ 24dp
+ 72dp
+ 60dp
+
+ 16dp
+ 16dp
+ 8dp
+ 160dp
+
diff --git a/V2rayNG/app/src/main/res/values/ic_launcher_background.xml b/V2rayNG/app/src/main/res/values/ic_launcher_background.xml
new file mode 100644
index 00000000..c5d5899f
--- /dev/null
+++ b/V2rayNG/app/src/main/res/values/ic_launcher_background.xml
@@ -0,0 +1,4 @@
+
+
+ #FFFFFF
+
\ No newline at end of file
diff --git a/V2rayNG/app/src/main/res/values/strings.xml b/V2rayNG/app/src/main/res/values/strings.xml
new file mode 100644
index 00000000..4f780629
--- /dev/null
+++ b/V2rayNG/app/src/main/res/values/strings.xml
@@ -0,0 +1,231 @@
+
+
+ v2rayNG
+ Switch
+ Switch
+ First use of this feature, please use the app to add server
+ Open navigation drawer
+ Close navigation drawer
+ Data migration success!
+ Data migration failed!
+
+
+ Stop
+ Unable to obtain the permission
+ click for more
+ Start Services
+ Stop Services
+ Start Services Success
+ Start Services Failure
+
+
+ Configuration file
+ Add config
+ Save config
+ Delete config
+ Import config from QRcode
+ Import config from Clipboard
+ Type manually[Vmess]
+ Type manually[VLESS]
+ Type manually[Shadowsocks]
+ Type manually[Socks]
+ Type manually[Trojan]
+ custom config
+ Import custom config from Clipboard
+ Import custom config from locally
+ Import custom config from URL
+ Import custom config scan URL
+ Confirm delete?
+ remarks
+ address
+ port
+ id
+ alterId
+ security
+ Network
+ Transport
+ head type
+ gRPC mode
+ request host(host/ws host/h2 host)/QUIC security
+ path(ws path/h2 path)/QUIC key/kcp seed/gRPC serviceName
+ tls
+ uTLS
+ alpn
+ allowInsecure
+ SNI
+ address
+ port
+ password
+ security
+ Password(Optional)
+ User(Optional)
+ encryption
+ flow
+ Success
+ Failure
+ There is nothing
+ Incorrect protocol
+ Decoding failed
+ Select a Config File
+ Please install a File Manager.
+ Customize Config
+ Invalid Config
+ Content
+ There is no data in the clipboard
+ Invalid URL
+ Ensure inbounds port is consistent with the settings
+ Config malformed
+ Host(SNI)(Optional)
+ File copy failed, please use File Manager
+ Add files
+ Download files
+
+
+ Loading
+ Search
+ Select all
+ Enter keywords
+ Bypass Mode
+ Auto select proxy app
+ Downloading content
+ Export to Clipboard
+ Import from Clipboard
+
+
+
+ Settings
+ Advanced Settings
+ VPN Settings
+ Per-app proxy
+ General: Checked App is proxy, unchecked direct connection; \nbypass mode: checked app directly connected, unchecked proxy. \nThe option to automatically select the proxy application in the menu
+
+ Enable Mux
+ Enable maybe speed up network and switch network maybe flash
+
+ Enable speed display
+ Display current speed in the notification.\nNotification icon would change based on
+ usage.
+
+ Enable Sniffing
+ Try sniff domain from the packet (default on)
+
+ Enable local DNS
+ DNS processed by core‘s DNS module (Recommended, if need routing Bypassing LAN and
+ mainland address)
+
+ Enable fake DNS
+ local DNS returns fake IP address (faster, but it may not work for some apps)
+
+ Prefer IPv6
+ Prefer IPv6 address and routes
+
+ Routing
+ Domain strategy
+ Predefined rules
+ Custom rules
+
+ Remote DNS (Optional)
+ DNS
+
+ VPN DNS (only IPv4/v6)
+
+ Domestic DNS (Optional)
+ DNS
+
+ Allow connections from the LAN
+ Other devices can connect to proxy by your ip address through socks/http, Only enable in trusted network to avoid unauthorized connection
+ Allow connections from the LAN, Make sure you are in a trusted network
+
+ allowInsecure
+ When TLS, the default allowInsecure
+
+ SOCKS5 proxy port
+ SOCKS5 proxy port
+
+ HTTP proxy port
+ HTTP proxy port
+
+ Local DNS port
+ Local DNS port
+
+ Delete configuration file confirmation
+ Whether to delete the configuration file requires a second confirmation by the user
+
+ Feedback
+ Feedback enhancements or bugs to GitHub
+ Join Telegram Group
+ Telegram app not found
+
+ Promotion
+ Promotion,click for details(Donation can be removed)
+
+ Log Level
+ Mode
+ Click me for more help
+ Language
+
+ Logcat
+ Copy
+ Clear
+ Service restart
+ Delete all config
+ Delete invalid config(Test first)
+ Export non-custom configs to clipboard
+ Subscription group setting
+ remarks
+ Optional URL
+ enable update
+ Update subscription
+ Tcping all configuration
+ Real delay all configuration
+ Geo asset files
+ Sorting by test results
+ Filter configuration file
+ All subscription groups
+
+ Start Service
+ Confirm
+
+ Routing Settings
+ Separated by commas(,),remember to save
+ Save
+ Clear
+ Scan and replace
+ Scan and append
+ set default routing rules
+
+ Check Connectivity
+ Testing…
+ Success: HTTP connection took %dms
+ Fail to detect internet connection: %s
+ Internet Unavailable
+ Error code: #%d
+ Connected, tap to check connection
+ Not connected
+
+
+ - QRcode
+ - Export to clipboard
+ - Export full configuration to clipboard
+
+
+
+ - proxy URL or IP
+ - direct URL or IP
+ - blocked URL or IP
+
+
+
+ - Global proxy
+ - Bypassing the LAN address then proxy
+ - Bypass mainland address then proxy
+ - Bypassing LAN and mainland address then proxy
+ - Global direct
+
+
+
+ - VPN
+ - Proxy only
+
+
+
diff --git a/V2rayNG/app/src/main/res/values/styles.xml b/V2rayNG/app/src/main/res/values/styles.xml
new file mode 100644
index 00000000..d775e998
--- /dev/null
+++ b/V2rayNG/app/src/main/res/values/styles.xml
@@ -0,0 +1,52 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/V2rayNG/app/src/main/res/xml/app_widget_provider.xml b/V2rayNG/app/src/main/res/xml/app_widget_provider.xml
new file mode 100644
index 00000000..f457545f
--- /dev/null
+++ b/V2rayNG/app/src/main/res/xml/app_widget_provider.xml
@@ -0,0 +1,6 @@
+
+
\ No newline at end of file
diff --git a/V2rayNG/app/src/main/res/xml/pref_settings.xml b/V2rayNG/app/src/main/res/xml/pref_settings.xml
new file mode 100644
index 00000000..73cec7d9
--- /dev/null
+++ b/V2rayNG/app/src/main/res/xml/pref_settings.xml
@@ -0,0 +1,141 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/V2rayNG/app/src/main/res/xml/shortcuts.xml b/V2rayNG/app/src/main/res/xml/shortcuts.xml
new file mode 100644
index 00000000..3e5f3656
--- /dev/null
+++ b/V2rayNG/app/src/main/res/xml/shortcuts.xml
@@ -0,0 +1,45 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/V2rayNG/app/src/test/java/com/v2ray/ang/ExampleUnitTest.java b/V2rayNG/app/src/test/java/com/v2ray/ang/ExampleUnitTest.java
new file mode 100644
index 00000000..5c6cdaa6
--- /dev/null
+++ b/V2rayNG/app/src/test/java/com/v2ray/ang/ExampleUnitTest.java
@@ -0,0 +1,15 @@
+package com.v2ray.ang;
+
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * To work on unit tests, switch the Test Artifact in the Build Variants view.
+ */
+public class ExampleUnitTest {
+ @Test
+ public void addition_isCorrect() throws Exception {
+ assertEquals(4, 2 + 2);
+ }
+}
\ No newline at end of file
diff --git a/V2rayNG/app/src/test/kotlin/com/v2ray/ang/ExampleUnitTest.kt b/V2rayNG/app/src/test/kotlin/com/v2ray/ang/ExampleUnitTest.kt
new file mode 100644
index 00000000..dd21d55a
--- /dev/null
+++ b/V2rayNG/app/src/test/kotlin/com/v2ray/ang/ExampleUnitTest.kt
@@ -0,0 +1,37 @@
+import org.junit.Assert.*
+import org.junit.Test
+import com.v2ray.ang.util.Utils
+
+class UtilTest {
+
+ @Test
+ fun test_parseInt() {
+ assertEquals(Utils.parseInt("1234"), 1234)
+ }
+
+ @Test
+ fun test_isIpAddress() {
+ assertFalse(Utils.isIpAddress("114.113.112.266"))
+ assertFalse(Utils.isIpAddress("666.666.666.666"))
+ assertFalse(Utils.isIpAddress("256.0.0.0" ))
+ assertFalse(Utils.isIpAddress("::ffff:127.0.0.0.1" ))
+ assertFalse(Utils.isIpAddress("baidu.com"))
+ assertFalse(Utils.isIpAddress(""))
+
+ assertTrue(Utils.isIpAddress("127.0.0.1" ))
+ assertTrue(Utils.isIpAddress("127.0.0.1:80" ))
+ assertTrue(Utils.isIpAddress("::1" ))
+ assertTrue(Utils.isIpAddress("[::1]:80" ))
+ assertTrue(Utils.isIpAddress("2605:2700:0:3::4713:93e3" ))
+ assertTrue(Utils.isIpAddress("[2605:2700:0:3::4713:93e3]:80" ))
+ assertTrue(Utils.isIpAddress("::ffff:192.168.173.22" ))
+ assertTrue(Utils.isIpAddress("[::ffff:192.168.173.22]:80" ))
+ assertTrue(Utils.isIpAddress("1::" ))
+ assertTrue(Utils.isIpAddress("::" ))
+ assertTrue(Utils.isIpAddress("10.24.56.0/24" ))
+ assertTrue(Utils.isIpAddress("2001:4321::1" ))
+ assertTrue(Utils.isIpAddress("240e:1234:abcd:12::6666" ))
+ assertTrue(Utils.isIpAddress("240e:1234:abcd:12::/64" ))
+ }
+}
+
diff --git a/V2rayNG/build.gradle b/V2rayNG/build.gradle
new file mode 100644
index 00000000..96c7a55a
--- /dev/null
+++ b/V2rayNG/build.gradle
@@ -0,0 +1,31 @@
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+
+buildscript {
+ repositories {
+ google()
+ mavenCentral()
+ maven { url 'https://maven.google.com' }
+ maven { url 'https://jitpack.io' }
+ }
+ dependencies {
+ classpath 'com.android.tools.build:gradle:7.2.1'
+ classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"
+
+ // NOTE: Do not place your application dependencies here; they belong
+ // in the individual module build.gradle files
+ }
+}
+
+allprojects {
+ repositories {
+ google()
+ mavenCentral()
+ maven { url 'https://maven.google.com' }
+ maven { url 'https://jitpack.io' }
+ jcenter()
+ }
+}
+
+task clean(type: Delete) {
+ delete rootProject.buildDir
+}
diff --git a/V2rayNG/gradle.properties b/V2rayNG/gradle.properties
new file mode 100644
index 00000000..1fa9a591
--- /dev/null
+++ b/V2rayNG/gradle.properties
@@ -0,0 +1,22 @@
+## Project-wide Gradle settings.
+#
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+#
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+# Default value: -Xmx1024m -XX:MaxPermSize=256m
+ org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
+#
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. More details, visit
+# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
+# org.gradle.parallel=true
+#Fri Jun 02 14:08:42 CST 2017
+kotlinVersion=1.6.21
+buildToolsVer=31.0.0
+compileSdkVer=31
+targetSdkVer=31
+kotlin.incremental=true
+android.useAndroidX=true
+android.enableJetifier=true
diff --git a/V2rayNG/gradle/wrapper/gradle-wrapper.jar b/V2rayNG/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 00000000..13372aef
Binary files /dev/null and b/V2rayNG/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/V2rayNG/gradle/wrapper/gradle-wrapper.properties b/V2rayNG/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 00000000..047ecba2
--- /dev/null
+++ b/V2rayNG/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Sun Jun 21 12:33:19 CST 2020
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-all.zip
diff --git a/V2rayNG/gradlew b/V2rayNG/gradlew
new file mode 100644
index 00000000..9d82f789
--- /dev/null
+++ b/V2rayNG/gradlew
@@ -0,0 +1,160 @@
+#!/usr/bin/env bash
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS=""
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn ( ) {
+ echo "$*"
+}
+
+die ( ) {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+esac
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+ JAVACMD=`cygpath --unix "$JAVACMD"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=$((i+1))
+ done
+ case $i in
+ (0) set -- ;;
+ (1) set -- "$args0" ;;
+ (2) set -- "$args0" "$args1" ;;
+ (3) set -- "$args0" "$args1" "$args2" ;;
+ (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
+function splitJvmOpts() {
+ JVM_OPTS=("$@")
+}
+eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
+JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
+
+exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
diff --git a/V2rayNG/gradlew.bat b/V2rayNG/gradlew.bat
new file mode 100644
index 00000000..8a0b282a
--- /dev/null
+++ b/V2rayNG/gradlew.bat
@@ -0,0 +1,90 @@
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS=
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto init
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto init
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:init
+@rem Get command-line arguments, handling Windowz variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_args
+if "%@eval[2+2]" == "4" goto 4NT_args
+
+:win9xME_args
+@rem Slurp the command line arguments.
+set CMD_LINE_ARGS=
+set _SKIP=2
+
+:win9xME_args_slurp
+if "x%~1" == "x" goto execute
+
+set CMD_LINE_ARGS=%*
+goto execute
+
+:4NT_args
+@rem Get arguments from the 4NT Shell from JP Software
+set CMD_LINE_ARGS=%$
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/V2rayNG/settings.gradle b/V2rayNG/settings.gradle
new file mode 100644
index 00000000..e7b4def4
--- /dev/null
+++ b/V2rayNG/settings.gradle
@@ -0,0 +1 @@
+include ':app'