.
34
.github/ISSUE_TEMPLATE/bug_cn.md
vendored
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
---
|
||||||
|
name: v2rayNG程序问题
|
||||||
|
about: 创建一个报告来帮助我们改进
|
||||||
|
---
|
||||||
|
|
||||||
|
在提出问题前请先自行排除服务器端问题,同时也请通过搜索确认是否有人提出过相同问题。
|
||||||
|
|
||||||
|
|
||||||
|
### 预期行为
|
||||||
|
描述你认为应该发生什么
|
||||||
|
|
||||||
|
### 实际行为
|
||||||
|
描述实际发生了什么
|
||||||
|
|
||||||
|
### 复现方法
|
||||||
|
1.
|
||||||
|
2.
|
||||||
|
3.
|
||||||
|
|
||||||
|
### 日志信息
|
||||||
|
<details>
|
||||||
|
|
||||||
|
通过`adb logcat -s com.v2ray.ang GoLog V2rayConfigUtilGoLog Main`获取日志。请自行删减日志中可能出现的敏感信息。
|
||||||
|
|
||||||
|
如果问题可重现,建议先执行`adb logcat -c`清空系统日志再执行上述命令,再操作重现问题。
|
||||||
|
```
|
||||||
|
在这里粘贴日志
|
||||||
|
```
|
||||||
|
</details>
|
||||||
|
|
||||||
|
### 环境信息
|
||||||
|
|
||||||
|
### 额外信息(可选)
|
||||||
|
|
5
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
blank_issues_enabled: false
|
||||||
|
contact_links:
|
||||||
|
- name: V2Ray程序问题
|
||||||
|
url: https://github.com/v2fly/v2ray-core/
|
||||||
|
about: 如果您有V2Ray而非v2rayNG的问题,请至这个链接讨论。
|
5
.gitignore
vendored
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
*.dat
|
||||||
|
*.jks
|
||||||
|
V2rayNG/app/release/output.json
|
||||||
|
.idea/
|
||||||
|
.gradle/
|
20
AndroidLibV2rayLite/README.md
Normal file
|
@ -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:<wherever you ndk is located>
|
||||||
|
- 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)
|
29
CR.md
Normal file
|
@ -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 天通过我们的可用渠道和应用内提示来通知用户。**在新条款生效后继续使用软件即表示您同意修改后的隐私条款。**
|
674
LICENSE
Normal file
|
@ -0,0 +1,674 @@
|
||||||
|
GNU GENERAL PUBLIC LICENSE
|
||||||
|
Version 3, 29 June 2007
|
||||||
|
|
||||||
|
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
|
||||||
|
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.
|
||||||
|
|
||||||
|
<one line to give the program's name and a brief idea of what it does.>
|
||||||
|
Copyright (C) <year> <name of author>
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
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:
|
||||||
|
|
||||||
|
<program> Copyright (C) <year> <name of author>
|
||||||
|
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
|
||||||
|
<http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
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
|
||||||
|
<http://www.gnu.org/philosophy/why-not-lgpl.html>.
|
36
README.md
Normal file
|
@ -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)
|
||||||
|
|
||||||
|
<a href="https://play.google.com/store/apps/details?id=com.v2ray.ang">
|
||||||
|
<img alt="Get it on Google Play" src="https://play.google.com/intl/en_us/badges/images/generic/en_badge_web_generic.png" width="165" height="64" />
|
||||||
|
</a>
|
||||||
|
|
||||||
|
### 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`
|
10
V2rayNG/.gitignore
vendored
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
*.iml
|
||||||
|
.gradle
|
||||||
|
/local.properties
|
||||||
|
/.idea
|
||||||
|
.DS_Store
|
||||||
|
/build
|
||||||
|
/captures
|
||||||
|
*.apk
|
||||||
|
signing.properties
|
||||||
|
*.aar
|
2
V2rayNG/app/.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
/build
|
||||||
|
/google-services.json
|
151
V2rayNG/app/build.gradle
Normal file
|
@ -0,0 +1,151 @@
|
||||||
|
apply plugin: 'com.android.application'
|
||||||
|
apply plugin: 'kotlin-android'
|
||||||
|
|
||||||
|
Properties props = new Properties()
|
||||||
|
props.load(new FileInputStream(new File('local.properties')))
|
||||||
|
|
||||||
|
android {
|
||||||
|
compileSdkVersion Integer.parseInt("$compileSdkVer")
|
||||||
|
buildToolsVersion "$buildToolsVer"
|
||||||
|
|
||||||
|
compileOptions {
|
||||||
|
targetCompatibility = "8"
|
||||||
|
sourceCompatibility = "8"
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
applicationId "com.v2ray.ang"
|
||||||
|
minSdkVersion 21
|
||||||
|
targetSdkVersion Integer.parseInt("$targetSdkVer")
|
||||||
|
multiDexEnabled true
|
||||||
|
versionCode 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' }
|
||||||
|
// }
|
||||||
|
//}
|
BIN
V2rayNG/app/libs/arm64-v8a/libtun2socks.so
Normal file
BIN
V2rayNG/app/libs/armeabi-v7a/libtun2socks.so
Normal file
BIN
V2rayNG/app/libs/x86/libtun2socks.so
Normal file
BIN
V2rayNG/app/libs/x86_64/libtun2socks.so
Normal file
0
V2rayNG/app/proguard-rules.pro
vendored
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
package com.v2ray.ang;
|
||||||
|
|
||||||
|
import android.app.Application;
|
||||||
|
import android.test.ApplicationTestCase;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <a href="http://d.android.com/tools/testing/testing_android.html">Testing Fundamentals</a>
|
||||||
|
*/
|
||||||
|
public class ApplicationTest extends ApplicationTestCase<Application> {
|
||||||
|
public ApplicationTest() {
|
||||||
|
super(Application.class);
|
||||||
|
}
|
||||||
|
}
|
187
V2rayNG/app/src/main/AndroidManifest.xml
Normal file
|
@ -0,0 +1,187 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
package="com.v2ray.ang">
|
||||||
|
|
||||||
|
<supports-screens
|
||||||
|
android:anyDensity="true"
|
||||||
|
android:smallScreens="true"
|
||||||
|
android:normalScreens="true"
|
||||||
|
android:largeScreens="true"
|
||||||
|
android:xlargeScreens="true"/>
|
||||||
|
|
||||||
|
<uses-feature android:name="android.hardware.camera" android:required="false"/>
|
||||||
|
<uses-feature android:name="android.hardware.camera.autofocus" android:required="false"/>
|
||||||
|
|
||||||
|
<!-- https://developer.android.com/about/versions/11/privacy/package-visibility -->
|
||||||
|
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"
|
||||||
|
tools:ignore="QueryAllPackagesPermission" />
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
|
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
|
||||||
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||||
|
<!-- <useapplications-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" /> -->
|
||||||
|
<uses-permission android:name="android.permission.CAMERA" />
|
||||||
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
|
<!-- <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> -->
|
||||||
|
|
||||||
|
<application
|
||||||
|
android:name=".AngApplication"
|
||||||
|
android:allowBackup="true"
|
||||||
|
android:icon="@mipmap/ic_launcher"
|
||||||
|
android:label="@string/app_name"
|
||||||
|
android:supportsRtl="true"
|
||||||
|
android:theme="@style/AppThemeLight"
|
||||||
|
android:usesCleartextTraffic="true"
|
||||||
|
tools:targetApi="m">
|
||||||
|
<activity
|
||||||
|
android:exported="true"
|
||||||
|
android:name=".ui.MainActivity"
|
||||||
|
android:launchMode="singleTask">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
|
</intent-filter>
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.service.quicksettings.action.QS_TILE_PREFERENCES" />
|
||||||
|
</intent-filter>
|
||||||
|
<meta-data
|
||||||
|
android:name="android.app.shortcuts"
|
||||||
|
android:resource="@xml/shortcuts" />
|
||||||
|
</activity>
|
||||||
|
<activity
|
||||||
|
android:exported="false"
|
||||||
|
android:name=".ui.ServerActivity"
|
||||||
|
android:windowSoftInputMode="stateUnchanged" />
|
||||||
|
<activity
|
||||||
|
android:exported="false"
|
||||||
|
android:name=".ui.ServerCustomConfigActivity"
|
||||||
|
android:windowSoftInputMode="stateUnchanged" />
|
||||||
|
<activity
|
||||||
|
android:exported="false"
|
||||||
|
android:name=".ui.SettingsActivity" />
|
||||||
|
<activity
|
||||||
|
android:exported="false"
|
||||||
|
android:name=".ui.PerAppProxyActivity" />
|
||||||
|
<activity
|
||||||
|
android:exported="false"
|
||||||
|
android:name=".ui.ScannerActivity" />
|
||||||
|
<activity
|
||||||
|
android:exported="false"
|
||||||
|
android:name=".ui.LogcatActivity" />
|
||||||
|
<activity
|
||||||
|
android:exported="false"
|
||||||
|
android:name=".ui.RoutingSettingsActivity"
|
||||||
|
android:windowSoftInputMode="stateUnchanged" />
|
||||||
|
<activity
|
||||||
|
android:exported="false"
|
||||||
|
android:name=".ui.SubSettingActivity" />
|
||||||
|
<activity
|
||||||
|
android:exported="false"
|
||||||
|
android:name=".ui.UserAssetActivity" />
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:exported="false"
|
||||||
|
android:name=".ui.SubEditActivity" />
|
||||||
|
<activity
|
||||||
|
android:exported="false"
|
||||||
|
android:name=".ui.ScScannerActivity" />
|
||||||
|
<activity
|
||||||
|
android:exported="false"
|
||||||
|
android:name=".ui.ScSwitchActivity"
|
||||||
|
android:excludeFromRecents="true"
|
||||||
|
android:process=":RunSoLibV2RayDaemon"
|
||||||
|
android:theme="@style/AppTheme.NoActionBar.Translucent" />
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:exported="true"
|
||||||
|
android:name=".ui.UrlSchemeActivity">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.SEND" />
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<data android:mimeType="text/plain" />
|
||||||
|
</intent-filter>
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.VIEW"/>
|
||||||
|
<category android:name="android.intent.category.BROWSABLE"/>
|
||||||
|
<category android:name="android.intent.category.DEFAULT"/>
|
||||||
|
<data android:scheme="v2rayng"
|
||||||
|
android:host="install-config" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
|
||||||
|
<service
|
||||||
|
android:name=".service.V2RayVpnService"
|
||||||
|
android:enabled="true"
|
||||||
|
android:exported="false"
|
||||||
|
android:label="@string/app_name"
|
||||||
|
android:permission="android.permission.BIND_VPN_SERVICE"
|
||||||
|
android:process=":RunSoLibV2RayDaemon">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.net.VpnService" />
|
||||||
|
</intent-filter>
|
||||||
|
<meta-data
|
||||||
|
android:name="android.net.VpnService.SUPPORTS_ALWAYS_ON"
|
||||||
|
android:value="true" />
|
||||||
|
</service>
|
||||||
|
|
||||||
|
<service android:name=".service.V2RayProxyOnlyService"
|
||||||
|
android:exported="false"
|
||||||
|
android:label="@string/app_name"
|
||||||
|
android:process=":RunSoLibV2RayDaemon">
|
||||||
|
</service>
|
||||||
|
|
||||||
|
<service android:name=".service.V2RayTestService"
|
||||||
|
android:exported="false"
|
||||||
|
android:process=":RunSoLibV2RayDaemon">
|
||||||
|
</service>
|
||||||
|
|
||||||
|
<receiver
|
||||||
|
android:exported="true"
|
||||||
|
android:name=".receiver.WidgetProvider"
|
||||||
|
android:process=":RunSoLibV2RayDaemon">
|
||||||
|
<meta-data
|
||||||
|
android:name="android.appwidget.provider"
|
||||||
|
android:resource="@xml/app_widget_provider" />
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
||||||
|
<action android:name="com.v2ray.ang.action.widget.click" />
|
||||||
|
<action android:name="com.v2ray.ang.action.activity" />
|
||||||
|
</intent-filter>
|
||||||
|
</receiver>
|
||||||
|
|
||||||
|
<service
|
||||||
|
android:exported="true"
|
||||||
|
android:name=".service.QSTileService"
|
||||||
|
android:icon="@drawable/ic_stat_name"
|
||||||
|
android:label="@string/app_tile_name"
|
||||||
|
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE"
|
||||||
|
android:process=":RunSoLibV2RayDaemon">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.service.quicksettings.action.QS_TILE" />
|
||||||
|
</intent-filter>
|
||||||
|
</service>
|
||||||
|
<!-- =====================Tasker===================== -->
|
||||||
|
<activity
|
||||||
|
android:exported="true"
|
||||||
|
android:name=".ui.TaskerActivity"
|
||||||
|
android:icon="@mipmap/ic_launcher"
|
||||||
|
android:label="@string/app_name">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="com.twofortyfouram.locale.intent.action.EDIT_SETTING" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
|
||||||
|
<receiver
|
||||||
|
android:exported="true"
|
||||||
|
android:name=".receiver.TaskerReceiver"
|
||||||
|
android:process=":RunSoLibV2RayDaemon">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="com.twofortyfouram.locale.intent.action.FIRE_SETTING" />
|
||||||
|
</intent-filter>
|
||||||
|
</receiver>
|
||||||
|
<!-- =====================Tasker===================== -->
|
||||||
|
|
||||||
|
</application>
|
||||||
|
|
||||||
|
</manifest>
|
1
V2rayNG/app/src/main/assets/custom_routing_block
Normal file
|
@ -0,0 +1 @@
|
||||||
|
geosite:category-ads-all,
|
132
V2rayNG/app/src/main/assets/custom_routing_direct
Normal file
|
@ -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,
|
33
V2rayNG/app/src/main/assets/custom_routing_proxy
Normal file
|
@ -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,
|
241
V2rayNG/app/src/main/assets/proxy_packagename.txt
Normal file
|
@ -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
|
105
V2rayNG/app/src/main/assets/v2ray_config.json
Normal file
|
@ -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": []
|
||||||
|
}
|
||||||
|
}
|
BIN
V2rayNG/app/src/main/ic_launcher-web.png
Normal file
After Width: | Height: | Size: 12 KiB |
|
@ -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 <strong>not</strong> at the end of a "drop" event.<br/>
|
||||||
|
* <br/>
|
||||||
|
* 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.<br/>
|
||||||
|
* <br/>
|
||||||
|
* 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);
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
|
@ -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);
|
||||||
|
|
||||||
|
}
|
|
@ -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.<br/>
|
||||||
|
* </br/>
|
||||||
|
* Expects the <code>RecyclerView.Adapter</code> to listen for {@link
|
||||||
|
* ItemTouchHelperAdapter} callbacks and the <code>RecyclerView.ViewHolder</code> 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
28
V2rayNG/app/src/main/kotlin/com/v2ray/ang/AngApplication.kt
Normal file
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
89
V2rayNG/app/src/main/kotlin/com/v2ray/ang/AppConfig.kt
Normal file
|
@ -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
|
||||||
|
}
|
32
V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/AngConfig.kt
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
package com.v2ray.ang.dto
|
||||||
|
|
||||||
|
data class AngConfig(
|
||||||
|
var index: Int,
|
||||||
|
var vmess: ArrayList<VmessBean>,
|
||||||
|
var subItem: ArrayList<SubItemBean>
|
||||||
|
) {
|
||||||
|
data class VmessBean(var guid: String = "123456",
|
||||||
|
var address: String = "v2ray.cool",
|
||||||
|
var port: Int = 10086,
|
||||||
|
var id: String = "a3482e88-686a-4a58-8126-99c9df64b7bf",
|
||||||
|
var alterId: Int = 64,
|
||||||
|
var security: String = "aes-128-cfb",
|
||||||
|
var network: String = "tcp",
|
||||||
|
var remarks: String = "def",
|
||||||
|
var headerType: String = "",
|
||||||
|
var requestHost: String = "",
|
||||||
|
var path: String = "",
|
||||||
|
var streamSecurity: String = "",
|
||||||
|
var allowInsecure: String = "",
|
||||||
|
var configType: Int = 1,
|
||||||
|
var configVersion: Int = 1,
|
||||||
|
var testResult: String = "",
|
||||||
|
var subid: String = "",
|
||||||
|
var flow: String = "",
|
||||||
|
var sni: String = "")
|
||||||
|
|
||||||
|
data class SubItemBean(var id: String = "",
|
||||||
|
var remarks: String = "",
|
||||||
|
var url: String = "",
|
||||||
|
var enabled: Boolean = true)
|
||||||
|
}
|
9
V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/AppInfo.kt
Normal file
|
@ -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)
|
15
V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/EConfigType.kt
Normal file
|
@ -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 }
|
||||||
|
}
|
||||||
|
}
|
|
@ -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");
|
||||||
|
}
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<String> {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,4 @@
|
||||||
|
package com.v2ray.ang.dto
|
||||||
|
|
||||||
|
data class ServersCache(val guid: String,
|
||||||
|
val config: ServerConfig)
|
|
@ -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()) {
|
||||||
|
}
|
462
V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/V2rayConfig.kt
Normal file
|
@ -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<InboundBean>,
|
||||||
|
var outbounds: ArrayList<OutboundBean>,
|
||||||
|
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<String>,
|
||||||
|
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<VnextBean>? = null,
|
||||||
|
var servers: List<ServersBean>? = 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<WireGuardBean>? = null,
|
||||||
|
) {
|
||||||
|
|
||||||
|
data class VnextBean(var address: String = "",
|
||||||
|
var port: Int = DEFAULT_PORT,
|
||||||
|
var users: List<UsersBean>) {
|
||||||
|
|
||||||
|
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<SocksUsersBean>? = 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<String> = ArrayList(),
|
||||||
|
var headers: HeadersBean = HeadersBean(),
|
||||||
|
val version: String? = null,
|
||||||
|
val method: String? = null) {
|
||||||
|
data class HeadersBean(var Host: List<String> = ArrayList(),
|
||||||
|
@SerializedName("User-Agent")
|
||||||
|
val userAgent: List<String>? = null,
|
||||||
|
@SerializedName("Accept-Encoding")
|
||||||
|
val acceptEncoding: List<String>? = null,
|
||||||
|
val Connection: List<String>? = null,
|
||||||
|
val Pragma: String? = null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class KcpSettingsBean(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<String> = ArrayList(),
|
||||||
|
var path: String = "")
|
||||||
|
|
||||||
|
data class TlsSettingsBean(var allowInsecure: Boolean = false,
|
||||||
|
var serverName: String = "",
|
||||||
|
val alpn: List<String>? = null,
|
||||||
|
val minVersion: String? = null,
|
||||||
|
val maxVersion: String? = null,
|
||||||
|
val preferServerCipherSuites: Boolean? = null,
|
||||||
|
val cipherSuites: String? = null,
|
||||||
|
val fingerprint: String? = null,
|
||||||
|
val certificates: List<Any>? = null,
|
||||||
|
val disableSystemRoot: Boolean? = null,
|
||||||
|
val enableSessionResumption: Boolean? = null)
|
||||||
|
|
||||||
|
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<String>? {
|
||||||
|
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<Any>? = null,
|
||||||
|
var hosts: Map<String, Any>? = 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<String>? = null,
|
||||||
|
var expectIPs: List<String>? = null,
|
||||||
|
val clientIp: String? = null)
|
||||||
|
}
|
||||||
|
|
||||||
|
data class RoutingBean(var domainStrategy: String,
|
||||||
|
var domainMatcher: String? = null,
|
||||||
|
var rules: ArrayList<RulesBean>,
|
||||||
|
val balancers: List<Any>? = null) {
|
||||||
|
|
||||||
|
data class RulesBean(var type: String = "",
|
||||||
|
var ip: ArrayList<String>? = null,
|
||||||
|
var domain: ArrayList<String>? = null,
|
||||||
|
var outboundTag: String = "",
|
||||||
|
var balancerTag: String? = null,
|
||||||
|
var port: String? = null,
|
||||||
|
val sourcePort: String? = null,
|
||||||
|
val network: String? = null,
|
||||||
|
val source: List<String>? = null,
|
||||||
|
val user: List<String>? = null,
|
||||||
|
var inboundTag: List<String>? = null,
|
||||||
|
val protocol: List<String>? = null,
|
||||||
|
val attrs: String? = null,
|
||||||
|
val domainMatcher: String? = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
data class PolicyBean(var levels: Map<String, LevelBean>,
|
||||||
|
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<Double>() {}.type,
|
||||||
|
JsonSerializer { src: Double?, _: Type?, _: JsonSerializationContext? -> JsonPrimitive(src?.toInt()) }
|
||||||
|
)
|
||||||
|
.create()
|
||||||
|
.toJson(this)
|
||||||
|
}
|
||||||
|
}
|
16
V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/VmessQRCode.kt
Normal file
|
@ -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 = "")
|
80
V2rayNG/app/src/main/kotlin/com/v2ray/ang/extension/_Ext.kt
Normal file
|
@ -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<String, Any>) = putOpt(pair.first, pair.second)
|
||||||
|
fun JSONObject.putOpt(pairs: Map<String, Any>) = 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("]", "")
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<QSTileService> = 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<ServiceControl>? = 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<String, String>
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
53
V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/BaseActivity.kt
Normal file
|
@ -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)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
|
@ -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<Fragment>) :
|
||||||
|
FragmentStateAdapter(fragmentActivity) {
|
||||||
|
|
||||||
|
override fun createFragment(position: Int): Fragment {
|
||||||
|
return mFragments[position]
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItemCount(): Int {
|
||||||
|
return mFragments.size
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<String>()
|
||||||
|
lst.add("logcat")
|
||||||
|
lst.add("-c")
|
||||||
|
val process = Runtime.getRuntime().exec(lst.toTypedArray())
|
||||||
|
process.waitFor()
|
||||||
|
}
|
||||||
|
val lst = LinkedHashSet<String>()
|
||||||
|
lst.add("logcat")
|
||||||
|
lst.add("-d")
|
||||||
|
lst.add("-v")
|
||||||
|
lst.add("time")
|
||||||
|
lst.add("-s")
|
||||||
|
lst.add("GoLog,tun2socks,${ANG_PACKAGE},AndroidRuntime,System.err")
|
||||||
|
val 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)
|
||||||
|
}
|
||||||
|
}
|
658
V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/MainActivity.kt
Normal file
|
@ -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<SubSettingActivity>()
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<MainRecyclerAdapter.BaseViewHolder>()
|
||||||
|
, 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<out String> 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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<AppInfo>? = 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<AppInfo> { p1, p2 ->
|
||||||
|
when {
|
||||||
|
p1.isSelected > p2.isSelected -> -1
|
||||||
|
p1.isSelected == p2.isSelected -> 0
|
||||||
|
else -> 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
it.sortedWith(comparator)
|
||||||
|
} else {
|
||||||
|
val comparator = object : Comparator<AppInfo> {
|
||||||
|
val collator = Collator.getInstance()
|
||||||
|
override fun compare(o1: AppInfo, o2: AppInfo) = collator.compare(o1.appName, o2.appName)
|
||||||
|
}
|
||||||
|
it.sortedWith(comparator)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// .map {
|
||||||
|
// val comparator = object : Comparator<AppInfo> {
|
||||||
|
// val collator = Collator.getInstance()
|
||||||
|
// override fun compare(o1: AppInfo, o2: AppInfo) = collator.compare(o1.appName, o2.appName)
|
||||||
|
// }
|
||||||
|
// it.sortedWith(comparator)
|
||||||
|
// }
|
||||||
|
.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<AppInfo>()
|
||||||
|
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<AppInfo>()
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<AppInfo>, blacklist: MutableSet<String>?) :
|
||||||
|
RecyclerView.Adapter<PerAppProxyAdapter.BaseViewHolder>() {
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,38 @@
|
||||||
|
package com.v2ray.ang.ui
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import com.v2ray.ang.R
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import com.google.android.material.tabs.TabLayoutMediator
|
||||||
|
import com.v2ray.ang.AppConfig
|
||||||
|
import com.v2ray.ang.databinding.ActivityRoutingSettingsBinding
|
||||||
|
|
||||||
|
class RoutingSettingsActivity : BaseActivity() {
|
||||||
|
private lateinit var binding: ActivityRoutingSettingsBinding
|
||||||
|
|
||||||
|
private val titles: Array<out String> by lazy {
|
||||||
|
resources.getStringArray(R.array.routing_tag)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
binding = ActivityRoutingSettingsBinding.inflate(layoutInflater)
|
||||||
|
val view = binding.root
|
||||||
|
setContentView(view)
|
||||||
|
|
||||||
|
title = getString(R.string.title_pref_routing_custom)
|
||||||
|
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||||
|
|
||||||
|
val fragments = ArrayList<Fragment>()
|
||||||
|
fragments.add(RoutingSettingsFragment().newInstance(AppConfig.PREF_V2RAY_ROUTING_AGENT))
|
||||||
|
fragments.add(RoutingSettingsFragment().newInstance(AppConfig.PREF_V2RAY_ROUTING_DIRECT))
|
||||||
|
fragments.add(RoutingSettingsFragment().newInstance(AppConfig.PREF_V2RAY_ROUTING_BLOCKED))
|
||||||
|
|
||||||
|
val adapter = FragmentAdapter(this, fragments)
|
||||||
|
binding.viewpager.adapter = adapter
|
||||||
|
//tablayout.setTabTextColors(Color.BLACK, Color.RED)
|
||||||
|
TabLayoutMediator(binding.tablayout, binding.viewpager) { tab, position ->
|
||||||
|
tab.text = titles[position]
|
||||||
|
}.attach()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,158 @@
|
||||||
|
package com.v2ray.ang.ui
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
|
import android.app.Activity.RESULT_OK
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.text.TextUtils
|
||||||
|
import android.view.*
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.preference.PreferenceManager
|
||||||
|
import com.tbruyelle.rxpermissions.RxPermissions
|
||||||
|
import com.v2ray.ang.AppConfig
|
||||||
|
import com.v2ray.ang.R
|
||||||
|
import com.v2ray.ang.databinding.FragmentRoutingSettingsBinding
|
||||||
|
import com.v2ray.ang.extension.toast
|
||||||
|
import com.v2ray.ang.extension.v2RayApplication
|
||||||
|
import com.v2ray.ang.util.Utils
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.GlobalScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
class RoutingSettingsFragment : Fragment() {
|
||||||
|
private lateinit var binding: FragmentRoutingSettingsBinding
|
||||||
|
companion object {
|
||||||
|
private const val routing_arg = "routing_arg"
|
||||||
|
}
|
||||||
|
|
||||||
|
val defaultSharedPreferences by lazy { PreferenceManager.getDefaultSharedPreferences(requireContext()) }
|
||||||
|
|
||||||
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?): View? {
|
||||||
|
// Inflate the layout for this fragment
|
||||||
|
binding = FragmentRoutingSettingsBinding.inflate(layoutInflater)
|
||||||
|
return binding.root// inflater.inflate(R.layout.fragment_routing_settings, container, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun newInstance(arg: String): Fragment {
|
||||||
|
val fragment = RoutingSettingsFragment()
|
||||||
|
val bundle = Bundle()
|
||||||
|
bundle.putString(routing_arg, arg)
|
||||||
|
fragment.arguments = bundle
|
||||||
|
return fragment
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
|
val content = defaultSharedPreferences.getString(requireArguments().getString(routing_arg), "")
|
||||||
|
binding.etRoutingContent.text = Utils.getEditable(content!!)
|
||||||
|
|
||||||
|
setHasOptionsMenu(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||||
|
inflater.inflate(R.menu.menu_routing, menu)
|
||||||
|
return super.onCreateOptionsMenu(menu, inflater)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
|
||||||
|
R.id.save_routing -> {
|
||||||
|
saveRouting()
|
||||||
|
true
|
||||||
|
}
|
||||||
|
R.id.del_routing -> {
|
||||||
|
binding.etRoutingContent.text = null
|
||||||
|
true
|
||||||
|
}
|
||||||
|
R.id.scan_replace -> {
|
||||||
|
scanQRcode(true)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
R.id.scan_append -> {
|
||||||
|
scanQRcode(false)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
R.id.default_rules -> {
|
||||||
|
setDefaultRules()
|
||||||
|
true
|
||||||
|
}
|
||||||
|
else -> super.onOptionsItemSelected(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun saveRouting() {
|
||||||
|
val content = binding.etRoutingContent.text.toString()
|
||||||
|
defaultSharedPreferences.edit().putString(requireArguments().getString(routing_arg), content).apply()
|
||||||
|
activity?.toast(R.string.toast_success)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun scanQRcode(forReplace: Boolean): Boolean {
|
||||||
|
// try {
|
||||||
|
// startActivityForResult(Intent("com.google.zxing.client.android.SCAN")
|
||||||
|
// .addCategory(Intent.CATEGORY_DEFAULT)
|
||||||
|
// .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP), requestCode)
|
||||||
|
// } catch (e: Exception) {
|
||||||
|
RxPermissions(requireActivity())
|
||||||
|
.request(Manifest.permission.CAMERA)
|
||||||
|
.subscribe {
|
||||||
|
if (it)
|
||||||
|
if (forReplace)
|
||||||
|
scanQRCodeForReplace.launch(Intent(activity, ScannerActivity::class.java))
|
||||||
|
else
|
||||||
|
scanQRCodeForAppend.launch(Intent(activity, ScannerActivity::class.java))
|
||||||
|
else
|
||||||
|
activity?.toast(R.string.toast_permission_denied)
|
||||||
|
}
|
||||||
|
// }
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
private val scanQRCodeForReplace = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||||
|
if (it.resultCode == RESULT_OK) {
|
||||||
|
val content = it.data?.getStringExtra("SCAN_RESULT")
|
||||||
|
binding.etRoutingContent.text = Utils.getEditable(content!!)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val scanQRCodeForAppend = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||||
|
if (it.resultCode == RESULT_OK) {
|
||||||
|
val content = it.data?.getStringExtra("SCAN_RESULT")
|
||||||
|
binding.etRoutingContent.text = Utils.getEditable("${binding.etRoutingContent.text},$content")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setDefaultRules(): Boolean {
|
||||||
|
var url = AppConfig.v2rayCustomRoutingListUrl
|
||||||
|
var tag = ""
|
||||||
|
when (requireArguments().getString(routing_arg)) {
|
||||||
|
AppConfig.PREF_V2RAY_ROUTING_AGENT -> {
|
||||||
|
tag = AppConfig.TAG_AGENT
|
||||||
|
}
|
||||||
|
AppConfig.PREF_V2RAY_ROUTING_DIRECT -> {
|
||||||
|
tag = AppConfig.TAG_DIRECT
|
||||||
|
}
|
||||||
|
AppConfig.PREF_V2RAY_ROUTING_BLOCKED -> {
|
||||||
|
tag = AppConfig.TAG_BLOCKED
|
||||||
|
}
|
||||||
|
}
|
||||||
|
url += tag
|
||||||
|
|
||||||
|
activity?.toast(R.string.msg_downloading_content)
|
||||||
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
|
val content = Utils.getUrlContext(url, 5000)
|
||||||
|
launch(Dispatchers.Main) {
|
||||||
|
val routingList = if (TextUtils.isEmpty(content)) {
|
||||||
|
Utils.readTextFromAssets(activity?.v2RayApplication!!, "custom_routing_$tag")
|
||||||
|
} else {
|
||||||
|
content
|
||||||
|
}
|
||||||
|
binding.etRoutingContent.text = Utils.getEditable(routingList)
|
||||||
|
saveRouting()
|
||||||
|
//toast(R.string.toast_success)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
116
V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/ScannerActivity.kt
Normal file
|
@ -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<BarcodeFormat>()
|
||||||
|
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())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
412
V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/ServerActivity.kt
Normal file
|
@ -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<out String> by lazy {
|
||||||
|
resources.getStringArray(R.array.securitys)
|
||||||
|
}
|
||||||
|
private val shadowsocksSecuritys: Array<out String> by lazy {
|
||||||
|
resources.getStringArray(R.array.ss_securitys)
|
||||||
|
}
|
||||||
|
private val flows: Array<out String> by lazy {
|
||||||
|
resources.getStringArray(R.array.flows)
|
||||||
|
}
|
||||||
|
private val networks: Array<out String> by lazy {
|
||||||
|
resources.getStringArray(R.array.networks)
|
||||||
|
}
|
||||||
|
private val tcpTypes: Array<out String> by lazy {
|
||||||
|
resources.getStringArray(R.array.header_type_tcp)
|
||||||
|
}
|
||||||
|
private val kcpAndQuicTypes: Array<out String> by lazy {
|
||||||
|
resources.getStringArray(R.array.header_type_kcp_and_quic)
|
||||||
|
}
|
||||||
|
private val grpcModes: Array<out String> by lazy {
|
||||||
|
resources.getStringArray(R.array.mode_type_grpc)
|
||||||
|
}
|
||||||
|
private val streamSecuritys: Array<out String> by lazy {
|
||||||
|
resources.getStringArray(R.array.streamsecurityxs)
|
||||||
|
}
|
||||||
|
private val allowinsecures: Array<out String> by lazy {
|
||||||
|
resources.getStringArray(R.array.allowinsecures)
|
||||||
|
}
|
||||||
|
private val uTlsItems: Array<out String> by lazy {
|
||||||
|
resources.getStringArray(R.array.streamsecurity_utls)
|
||||||
|
}
|
||||||
|
private val alpns: Array<out String> by lazy {
|
||||||
|
resources.getStringArray(R.array.streamsecurity_alpn)
|
||||||
|
}
|
||||||
|
// Kotlin synthetics was used, but since it is removed in 1.8. We switch to old manual approach.
|
||||||
|
// 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<out String> {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
182
V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/SettingsActivity.kt
Normal file
|
@ -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<CheckBoxPreference>(AppConfig.PREF_PER_APP_PROXY) }
|
||||||
|
private val localDns by lazy { findPreference<CheckBoxPreference>(AppConfig.PREF_LOCAL_DNS_ENABLED) }
|
||||||
|
private val fakeDns by lazy { findPreference<CheckBoxPreference>(AppConfig.PREF_FAKE_DNS_ENABLED) }
|
||||||
|
private val localDnsPort by lazy { findPreference<EditTextPreference>(AppConfig.PREF_LOCAL_DNS_PORT) }
|
||||||
|
private val vpnDns by lazy { findPreference<EditTextPreference>(AppConfig.PREF_VPN_DNS) }
|
||||||
|
// val autoRestart by lazy { findPreference(PREF_AUTO_RESTART) as CheckBoxPreference }
|
||||||
|
private val remoteDns by lazy { findPreference<EditTextPreference>(AppConfig.PREF_REMOTE_DNS) }
|
||||||
|
private val domesticDns by lazy { findPreference<EditTextPreference>(AppConfig.PREF_DOMESTIC_DNS) }
|
||||||
|
private val socksPort by lazy { findPreference<EditTextPreference>(AppConfig.PREF_SOCKS_PORT) }
|
||||||
|
private val httpPort by lazy { findPreference<EditTextPreference>(AppConfig.PREF_HTTP_PORT) }
|
||||||
|
private val routingCustom by lazy { findPreference<Preference>(AppConfig.PREF_ROUTING_CUSTOM) }
|
||||||
|
// val licenses: Preference by lazy { findPreference(PREF_LICENSES) }
|
||||||
|
// val feedback: Preference by lazy { findPreference(PREF_FEEDBACK) }
|
||||||
|
// val tgGroup: Preference by lazy { findPreference(PREF_TG_GROUP) }
|
||||||
|
|
||||||
|
private val mode by lazy { findPreference<ListPreference>(AppConfig.PREF_MODE) }
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
133
V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/SubEditActivity.kt
Normal file
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<Pair<String, SubscriptionItem>> = 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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<SubSettingRecyclerAdapter.MainViewHolder>() {
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
117
V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/TaskerActivity.kt
Normal file
|
@ -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<String> = ArrayList()
|
||||||
|
private var lstGuid: ArrayList<String> = ArrayList()
|
||||||
|
|
||||||
|
private val serverStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_SERVER_CONFIG, MMKV.MULTI_PROCESS_MODE) }
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
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<View>(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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<UserAssetViewHolder>() {
|
||||||
|
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)
|
||||||
|
}
|
|
@ -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<String, String>()
|
||||||
|
if (config.configType == EConfigType.VLESS) {
|
||||||
|
outbound.settings?.vnext?.get(0)?.users?.get(0)?.flow?.let {
|
||||||
|
if (!TextUtils.isEmpty(it)) {
|
||||||
|
dicQuery["flow"] = it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dicQuery["encryption"] =
|
||||||
|
if (outbound.getSecurityEncryption().isNullOrEmpty()) "none"
|
||||||
|
else outbound.getSecurityEncryption().orEmpty()
|
||||||
|
} else if (config.configType == EConfigType.TROJAN) {
|
||||||
|
config.outboundBean?.settings?.servers?.get(0)?.flow?.let {
|
||||||
|
if (!TextUtils.isEmpty(it)) {
|
||||||
|
dicQuery["flow"] = it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dicQuery["security"] = streamSetting.security.ifEmpty { "none" }
|
||||||
|
(streamSetting.tlsSettings?: streamSetting.xtlsSettings)?.let { tlsSetting ->
|
||||||
|
if (!TextUtils.isEmpty(tlsSetting.serverName)) {
|
||||||
|
dicQuery["sni"] = tlsSetting.serverName
|
||||||
|
}
|
||||||
|
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<String>): 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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<AppInfo> {
|
||||||
|
val packageManager = ctx.packageManager
|
||||||
|
val packages = packageManager.getInstalledPackages(PackageManager.GET_PERMISSIONS)
|
||||||
|
val apps = ArrayList<AppInfo>()
|
||||||
|
|
||||||
|
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<ArrayList<AppInfo>> = Observable.unsafeCreate {
|
||||||
|
it.onNext(loadNetworkAppList(ctx))
|
||||||
|
}
|
||||||
|
|
||||||
|
val PackageInfo.hasInternetPermission: Boolean
|
||||||
|
get() {
|
||||||
|
val permissions = requestedPermissions
|
||||||
|
return permissions?.any { it == Manifest.permission.INTERNET } ?: false
|
||||||
|
}
|
||||||
|
}
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
179
V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/MmkvManager.kt
Normal file
|
@ -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<String> {
|
||||||
|
val json = mainStorage?.decodeString(KEY_ANG_CONFIGS)
|
||||||
|
return if (json.isNullOrBlank()) {
|
||||||
|
mutableListOf()
|
||||||
|
} else {
|
||||||
|
Gson().fromJson(json, Array<String>::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<Pair<String, SubscriptionItem>> {
|
||||||
|
val subscriptions = mutableListOf<Pair<String, SubscriptionItem>>()
|
||||||
|
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<ServerDelay>()
|
||||||
|
val serverList = decodeServerList()
|
||||||
|
serverList.forEach { key ->
|
||||||
|
val delay = decodeServerAffiliationInfo(key)?.testDelayMillis ?: 0L
|
||||||
|
serverDelays.add(ServerDelay(key, if (delay <= 0L) 999999 else delay))
|
||||||
|
}
|
||||||
|
serverDelays.sortBy { it.testDelayMillis }
|
||||||
|
|
||||||
|
serverDelays.forEach {
|
||||||
|
serverList.remove(it.guid)
|
||||||
|
serverList.add(it.guid)
|
||||||
|
}
|
||||||
|
|
||||||
|
mainStorage?.encode(KEY_ANG_CONFIGS, Gson().toJson(serverList))
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
100
V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/QRCodeDecoder.kt
Normal file
|
@ -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<DecodeHintType, Any?> = 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<BarcodeFormat> = arrayListOf(
|
||||||
|
BarcodeFormat.AZTEC
|
||||||
|
,BarcodeFormat.CODABAR
|
||||||
|
,BarcodeFormat.CODE_39
|
||||||
|
,BarcodeFormat.CODE_93
|
||||||
|
,BarcodeFormat.CODE_128
|
||||||
|
,BarcodeFormat.DATA_MATRIX
|
||||||
|
,BarcodeFormat.EAN_8
|
||||||
|
,BarcodeFormat.EAN_13
|
||||||
|
,BarcodeFormat.ITF
|
||||||
|
,BarcodeFormat.MAXICODE
|
||||||
|
,BarcodeFormat.PDF_417
|
||||||
|
,BarcodeFormat.QR_CODE
|
||||||
|
,BarcodeFormat.RSS_14
|
||||||
|
,BarcodeFormat.RSS_EXPANDED
|
||||||
|
,BarcodeFormat.UPC_A
|
||||||
|
,BarcodeFormat.UPC_E
|
||||||
|
,BarcodeFormat.UPC_EAN_EXTENSION)
|
||||||
|
HINTS[DecodeHintType.TRY_HARDER] = BarcodeFormat.QR_CODE
|
||||||
|
HINTS[DecodeHintType.POSSIBLE_FORMATS] = allFormats
|
||||||
|
HINTS[DecodeHintType.CHARACTER_SET] = "utf-8"
|
||||||
|
}
|
||||||
|
}
|
142
V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/SpeedtestUtil.kt
Normal file
|
@ -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<Socket?>()
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
439
V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/Utils.kt
Normal file
|
@ -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<out String>, 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<String> {
|
||||||
|
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<String> {
|
||||||
|
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<String> {
|
||||||
|
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<EncodeHintType, String>()
|
||||||
|
hints[EncodeHintType.CHARACTER_SET] = "utf-8"
|
||||||
|
val bitMatrix = QRCodeWriter().encode(text,
|
||||||
|
BarcodeFormat.QR_CODE, size, size, hints)
|
||||||
|
val pixels = IntArray(size * size)
|
||||||
|
for (y in 0 until size) {
|
||||||
|
for (x in 0 until size) {
|
||||||
|
if (bitMatrix.get(x, y)) {
|
||||||
|
pixels[y * size + x] = 0xff000000.toInt()
|
||||||
|
} else {
|
||||||
|
pixels[y * size + x] = 0xffffffff.toInt()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val bitmap = Bitmap.createBitmap(size, size,
|
||||||
|
Bitmap.Config.ARGB_8888)
|
||||||
|
bitmap.setPixels(pixels, 0, size, 0, 0, size, size)
|
||||||
|
return bitmap
|
||||||
|
} catch (e: WriterException) {
|
||||||
|
e.printStackTrace()
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* is ip address
|
||||||
|
*/
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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<String> {
|
||||||
|
val domain = ArrayList<String>()
|
||||||
|
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<String, String>()
|
||||||
|
val servers = ArrayList<Any>()
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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<ServersCache>()
|
||||||
|
val isRunning by lazy { MutableLiveData<Boolean>() }
|
||||||
|
val updateListAction by lazy { MutableLiveData<Int>() }
|
||||||
|
val updateTestResultAction by lazy { MutableLiveData<String>() }
|
||||||
|
|
||||||
|
private val tcpingTestScope by lazy { CoroutineScope(Dispatchers.IO) }
|
||||||
|
|
||||||
|
fun startListenBroadcast() {
|
||||||
|
isRunning.value = false
|
||||||
|
getApplication<AngApplication>().registerReceiver(mMsgReceiver, IntentFilter(AppConfig.BROADCAST_ACTION_ACTIVITY))
|
||||||
|
MessageUtil.sendMsg2Service(getApplication(), AppConfig.MSG_REGISTER_CLIENT, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCleared() {
|
||||||
|
getApplication<AngApplication>().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<AngApplication>().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<AngApplication>().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<String>( context, android.R.layout.simple_spinner_dropdown_item, listRemarks)
|
||||||
|
ivBinding.spSubscriptionId.setSelection(checkedItem)
|
||||||
|
ivBinding.etKeyword.text = Utils.getEditable(keywordFilter)
|
||||||
|
val builder = AlertDialog.Builder(context).setView(ivBinding.root)
|
||||||
|
builder.setTitle(R.string.title_filter_config)
|
||||||
|
builder.setPositiveButton(R.string.tasker_setting_confirm) { dialogInterface: DialogInterface?, _: Int ->
|
||||||
|
try {
|
||||||
|
val position = ivBinding.spSubscriptionId.selectedItemPosition
|
||||||
|
subscriptionId = if (listRemarks.count() - 1 == position) {
|
||||||
|
""
|
||||||
|
} else {
|
||||||
|
subscriptions[position].first
|
||||||
|
}
|
||||||
|
keywordFilter = ivBinding.etKeyword.text.toString()
|
||||||
|
reloadServerList()
|
||||||
|
|
||||||
|
dialogInterface?.dismiss()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
builder.show()
|
||||||
|
// AlertDialog.Builder(context)
|
||||||
|
// .setSingleChoiceItems(listRemarks.toTypedArray(), checkedItem) { dialog, i ->
|
||||||
|
// try {
|
||||||
|
// subscriptionId = if (listRemarks.count() - 1 == i) {
|
||||||
|
// ""
|
||||||
|
// } else {
|
||||||
|
// subscriptions[i].first
|
||||||
|
// }
|
||||||
|
// reloadServerList()
|
||||||
|
// dialog.dismiss()
|
||||||
|
// } catch (e: Exception) {
|
||||||
|
// e.printStackTrace()
|
||||||
|
// }
|
||||||
|
// }.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getPosition(guid: String) : Int {
|
||||||
|
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<AngApplication>().toast(R.string.toast_services_success)
|
||||||
|
isRunning.value = true
|
||||||
|
}
|
||||||
|
AppConfig.MSG_STATE_START_FAILURE -> {
|
||||||
|
getApplication<AngApplication>().toast(R.string.toast_services_failure)
|
||||||
|
isRunning.value = false
|
||||||
|
}
|
||||||
|
AppConfig.MSG_STATE_STOP_SUCCESS -> {
|
||||||
|
isRunning.value = false
|
||||||
|
}
|
||||||
|
AppConfig.MSG_MEASURE_DELAY_SUCCESS -> {
|
||||||
|
updateTestResultAction.value = intent.getStringExtra("content")
|
||||||
|
}
|
||||||
|
AppConfig.MSG_MEASURE_CONFIG_SUCCESS -> {
|
||||||
|
val resultPair = intent.getSerializableExtra("content") as Pair<String, Long>
|
||||||
|
MmkvManager.encodeServerTestDelayMillis(resultPair.first, resultPair.second)
|
||||||
|
updateListAction.value = getPosition(resultPair.first)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
6
V2rayNG/app/src/main/res/anim/fade_in.xml
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<alpha xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:duration="@android:integer/config_shortAnimTime"
|
||||||
|
android:fromAlpha="0.0"
|
||||||
|
android:interpolator="@android:interpolator/decelerate_quad"
|
||||||
|
android:toAlpha="1.0" />
|
6
V2rayNG/app/src/main/res/anim/fade_out.xml
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<alpha xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:duration="@android:integer/config_shortAnimTime"
|
||||||
|
android:fromAlpha="1.0"
|
||||||
|
android:interpolator="@android:interpolator/accelerate_quad"
|
||||||
|
android:toAlpha="0.0" />
|
|
@ -0,0 +1,5 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item android:alpha="@dimen/highlight_alpha_material_colored" android:color="?android:attr/colorControlActivated" android:state_checked="true" android:state_enabled="true" />
|
||||||
|
<item android:color="?android:attr/colorControlHighlight" />
|
||||||
|
</selector>
|
BIN
V2rayNG/app/src/main/res/drawable-hdpi/ic_stat_direct.png
Normal file
After Width: | Height: | Size: 964 B |
BIN
V2rayNG/app/src/main/res/drawable-hdpi/ic_stat_name.png
Normal file
After Width: | Height: | Size: 389 B |
BIN
V2rayNG/app/src/main/res/drawable-hdpi/ic_stat_proxy.png
Normal file
After Width: | Height: | Size: 1.2 KiB |
BIN
V2rayNG/app/src/main/res/drawable-mdpi/ic_stat_direct.png
Normal file
After Width: | Height: | Size: 739 B |
BIN
V2rayNG/app/src/main/res/drawable-mdpi/ic_stat_name.png
Normal file
After Width: | Height: | Size: 279 B |
BIN
V2rayNG/app/src/main/res/drawable-mdpi/ic_stat_proxy.png
Normal file
After Width: | Height: | Size: 882 B |
BIN
V2rayNG/app/src/main/res/drawable-xhdpi/ic_stat_direct.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
V2rayNG/app/src/main/res/drawable-xhdpi/ic_stat_name.png
Normal file
After Width: | Height: | Size: 494 B |
BIN
V2rayNG/app/src/main/res/drawable-xhdpi/ic_stat_proxy.png
Normal file
After Width: | Height: | Size: 1.5 KiB |
BIN
V2rayNG/app/src/main/res/drawable-xxhdpi/donate.png
Normal file
After Width: | Height: | Size: 3.2 KiB |
BIN
V2rayNG/app/src/main/res/drawable-xxhdpi/ic_stat_direct.png
Normal file
After Width: | Height: | Size: 1.5 KiB |
BIN
V2rayNG/app/src/main/res/drawable-xxhdpi/ic_stat_name.png
Normal file
After Width: | Height: | Size: 698 B |
BIN
V2rayNG/app/src/main/res/drawable-xxhdpi/ic_stat_proxy.png
Normal file
After Width: | Height: | Size: 2.3 KiB |
|
@ -0,0 +1,9 @@
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:shape="rectangle">
|
||||||
|
<gradient
|
||||||
|
android:angle="135"
|
||||||
|
android:centerColor="#009688"
|
||||||
|
android:endColor="#00695C"
|
||||||
|
android:startColor="#4DB6AC"
|
||||||
|
android:type="linear" />
|
||||||
|
</shape>
|
BIN
V2rayNG/app/src/main/res/drawable-xxxhdpi/ic_stat_direct.png
Normal file
After Width: | Height: | Size: 1.9 KiB |
BIN
V2rayNG/app/src/main/res/drawable-xxxhdpi/ic_stat_name.png
Normal file
After Width: | Height: | Size: 867 B |
BIN
V2rayNG/app/src/main/res/drawable-xxxhdpi/ic_stat_proxy.png
Normal file
After Width: | Height: | Size: 3.1 KiB |