Compare commits
306 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
613a643528 | ||
|
|
3144817795 | ||
|
|
6cae2c1785 | ||
|
|
a71509d130 | ||
|
|
d4aa419284 | ||
|
|
b484de5fb7 | ||
|
|
13480b0077 | ||
|
|
1987880294 | ||
|
|
0ae459a70e | ||
|
|
02bc46634d | ||
|
|
cfdaa0c54b | ||
|
|
cbe6c1e3e0 | ||
|
|
3cd6f12b94 | ||
|
|
516235cd6a | ||
|
|
236923d0a0 | ||
|
|
a4816b8251 | ||
|
|
2c2bc86457 | ||
|
|
68a03a93b5 | ||
|
|
5127a30ae9 | ||
|
|
8b76a7a4f4 | ||
|
|
b6aec3fd63 | ||
|
|
18e0dc4546 | ||
|
|
129c1db995 | ||
|
|
56d987713e | ||
|
|
35e57977ea | ||
|
|
34c14d1f1a | ||
|
|
bce08c5136 | ||
|
|
2d90a07788 | ||
|
|
b4204ed3a8 | ||
|
|
d30fb697bb | ||
|
|
18ff11dbba | ||
|
|
3923b379a6 | ||
|
|
e23e9c48a4 | ||
|
|
1822613985 | ||
|
|
e8ec9ad17c | ||
|
|
b4d970552e | ||
|
|
c6579556c4 | ||
|
|
5a36844036 | ||
|
|
2b21203c53 | ||
|
|
a4558cf954 | ||
|
|
5c2f11fb19 | ||
|
|
d7f3d0df80 | ||
|
|
c5e2ca0d8d | ||
|
|
252bea2432 | ||
|
|
f58ed74a4a | ||
|
|
8ed17f9da0 | ||
|
|
1bbfda64fe | ||
|
|
5cadef8b2a | ||
|
|
5b92158353 | ||
|
|
73706c1d0f | ||
|
|
c633a267ff | ||
|
|
c8e5bf4f9f | ||
|
|
bb91b3baa9 | ||
|
|
35dc8d661c | ||
|
|
f61f30fdc4 | ||
|
|
a54c327a07 | ||
|
|
87d2854fb2 | ||
|
|
e9b1052ef7 | ||
|
|
c6560e9bc0 | ||
|
|
a44ca16aa7 | ||
|
|
b28c7f54ff | ||
|
|
61a155b799 | ||
|
|
10cc117e81 | ||
|
|
ce9bed2e1f | ||
|
|
bb8f7de6eb | ||
|
|
a1ed4836c7 | ||
|
|
2d5351ec9e | ||
|
|
d9fb121d67 | ||
|
|
62d0951a24 | ||
|
|
d6605cc866 | ||
|
|
1fc493d879 | ||
|
|
bdc27dd180 | ||
|
|
e257c4cb56 | ||
|
|
e9d2ed98af | ||
|
|
901a82eb54 | ||
|
|
26cc29944b | ||
|
|
7052546f2b | ||
|
|
ae2b10ede7 | ||
|
|
0df051e640 | ||
|
|
79aa86a402 | ||
|
|
7bf32c2b30 | ||
|
|
ceb1c55e49 | ||
|
|
4ab3134eac | ||
|
|
0391685d42 | ||
|
|
6482253697 | ||
|
|
c033358126 | ||
|
|
0b52c78b72 | ||
|
|
a2a7d790e7 | ||
|
|
c7c6564624 | ||
|
|
f7d534fc00 | ||
|
|
ecb1d58e7a | ||
|
|
54a191d181 | ||
|
|
90b6979f94 | ||
|
|
5daf4886b7 | ||
|
|
dbee2085fb | ||
|
|
3c6c4ca3a5 | ||
|
|
3932ee6e9b | ||
|
|
242c96a4de | ||
|
|
a23245435f | ||
|
|
57476290d3 | ||
|
|
c5617ac65a | ||
|
|
3795348b04 | ||
|
|
50bf9c8da0 | ||
|
|
78fe4e4bc4 | ||
|
|
962519854d | ||
|
|
841629f9a3 | ||
|
|
d32ccc817a | ||
|
|
19cc665f2d | ||
|
|
031e9105e2 | ||
|
|
35c5d64863 | ||
|
|
0b05756d12 | ||
|
|
3548dcbb67 | ||
|
|
b897b2a0e9 | ||
|
|
eb60e4a0e4 | ||
|
|
c3dfa8cedc | ||
|
|
f58bf85b6d | ||
|
|
906d0714b5 | ||
|
|
701fed2525 | ||
|
|
e83208465f | ||
|
|
2b031033d3 | ||
|
|
e0e16b5934 | ||
|
|
0748f994ef | ||
|
|
233b34bda6 | ||
|
|
6ece3385fe | ||
|
|
9b8a810445 | ||
|
|
f63242d147 | ||
|
|
7024fabb82 | ||
|
|
459f52fec6 | ||
|
|
90f2d33d97 | ||
|
|
115008a8a4 | ||
|
|
8cdc7fb3c9 | ||
|
|
d0a2fa0086 | ||
|
|
f6c54841d2 | ||
|
|
54fa356999 | ||
|
|
9642b7f64f | ||
|
|
dd2d2c1638 | ||
|
|
e21950dbcd | ||
|
|
d016ab06d4 | ||
|
|
4d9aced5a4 | ||
|
|
62b928e6a0 | ||
|
|
0ce60eae73 | ||
|
|
5930a6a9eb | ||
|
|
a360310be2 | ||
|
|
820e6cdf36 | ||
|
|
658b890325 | ||
|
|
fb017c6659 | ||
|
|
00e6314afe | ||
|
|
463f45804f | ||
|
|
572955dd1e | ||
|
|
375a209beb | ||
|
|
872f9ce199 | ||
|
|
b4f02c9bd6 | ||
|
|
e567719f5b | ||
|
|
8407fc5825 | ||
|
|
a3e49dcc3d | ||
|
|
7b47bbe99a | ||
|
|
0fb2165015 | ||
|
|
03eeeb9b62 | ||
|
|
038daf5fda | ||
|
|
bfd1387d9b | ||
|
|
5afec5cf25 | ||
|
|
ec29bdf5bf | ||
|
|
57efab093f | ||
|
|
9c92a64811 | ||
|
|
7ddc82d5cd | ||
|
|
c286ba18a8 | ||
|
|
867b5fc880 | ||
|
|
e8a7fa5320 | ||
|
|
f2f9e55286 | ||
|
|
4a1c62a67c | ||
|
|
c9a6a459d4 | ||
|
|
21fdcf4ccf | ||
|
|
7c7a623ae5 | ||
|
|
b3074e9697 | ||
|
|
513ebcfa23 | ||
|
|
50d9057f1a | ||
|
|
2a563e7884 | ||
|
|
c69cd18842 | ||
|
|
7f2ced85a8 | ||
|
|
6c5eef99b5 | ||
|
|
d7c3bae8cc | ||
|
|
57c98f7c50 | ||
|
|
49be23c56a | ||
|
|
9b658e9a22 | ||
|
|
aaa84d081f | ||
|
|
5628cbee3a | ||
|
|
3dd663d927 | ||
|
|
9e49a2dbd9 | ||
|
|
6c199c1687 | ||
|
|
39af5fdb86 | ||
|
|
7b2794f6be | ||
|
|
411d9e5c9a | ||
|
|
a57aee9424 | ||
|
|
4602afc67e | ||
|
|
ceb29840f2 | ||
|
|
1c1f130ca7 | ||
|
|
afa0eb9375 | ||
|
|
49b682f0f3 | ||
|
|
d5def3bf2f | ||
|
|
51b97b64f2 | ||
|
|
3af9ce1a1a | ||
|
|
a5d3dda941 | ||
|
|
71a3e300d8 | ||
|
|
030b9a3900 | ||
|
|
24d105a53c | ||
|
|
45805d6df7 | ||
|
|
4437e6699b | ||
|
|
fa409f91e4 | ||
|
|
89be5f077b | ||
|
|
896889778f | ||
|
|
10f705a8b2 | ||
|
|
2956fa2030 | ||
|
|
c2a704a6ea | ||
|
|
78cac0cd90 | ||
|
|
3ffb2e8e05 | ||
|
|
e2d667e0bb | ||
|
|
3ae0777d7f | ||
|
|
5e6348676c | ||
|
|
df3f1ca3ef | ||
|
|
94f2bec329 | ||
|
|
c54d8fa43a | ||
|
|
5bbbdcf6f2 | ||
|
|
4abf20fa32 | ||
|
|
723727feb9 | ||
|
|
2efd4b741c | ||
|
|
83aab0f880 | ||
|
|
66ea17877e | ||
|
|
26bc985368 | ||
|
|
5bbf40c784 | ||
|
|
6d5c23245c | ||
|
|
b148290211 | ||
|
|
c473f9bb13 | ||
|
|
c7c3d27f36 | ||
|
|
bebc6fea13 | ||
|
|
9271857b1e | ||
|
|
7ea78c1840 | ||
|
|
ac668788b3 | ||
|
|
adfcf0a5d9 | ||
|
|
15b5595797 | ||
|
|
5a18296cb2 | ||
|
|
1256edbaf5 | ||
|
|
870950e807 | ||
|
|
e798cb3f42 | ||
|
|
b970f0bcff | ||
|
|
93b9a56428 | ||
|
|
eb8bd13266 | ||
|
|
d850c88c63 | ||
|
|
6bee795c0d | ||
|
|
d7d7e029e0 | ||
|
|
8efdab43d7 | ||
|
|
5e0235cf70 | ||
|
|
9f668b3da7 | ||
|
|
b2437279dc | ||
|
|
180b5efd93 | ||
|
|
dc31380cc2 | ||
|
|
1361e0dacf | ||
|
|
a45eb66bd1 | ||
|
|
508102cebe | ||
|
|
1d86dbb9f3 | ||
|
|
20ca554be2 | ||
|
|
25ba455656 | ||
|
|
17e7c62d53 | ||
|
|
6f0f2fdeda | ||
|
|
3c9c9b5a4c | ||
|
|
e13024d6bb | ||
|
|
9d58edd31f | ||
|
|
af7dfc3a43 | ||
|
|
ca554e6ac4 | ||
|
|
ec391d8689 | ||
|
|
6afd4d0549 | ||
|
|
957cf85362 | ||
|
|
881152e10a | ||
|
|
6e47ebb27a | ||
|
|
a731e6c360 | ||
|
|
d1466ba4b2 | ||
|
|
617f28f399 | ||
|
|
c0ec0d9404 | ||
|
|
3419dc8837 | ||
|
|
786aaf823a | ||
|
|
1144183da4 | ||
|
|
5bf2fda990 | ||
|
|
28639cc388 | ||
|
|
ca254b2aa1 | ||
|
|
9721879713 | ||
|
|
623c1807c5 | ||
|
|
739fa88ba7 | ||
|
|
85d6f00f8c | ||
|
|
8986710453 | ||
|
|
f54faacbf6 | ||
|
|
993ee0b8d2 | ||
|
|
903352ec9c | ||
|
|
ad56106c08 | ||
|
|
91b8284afd | ||
|
|
ef9e0cc0d2 | ||
|
|
6577c46a31 | ||
|
|
c6dab001b2 | ||
|
|
aea8369b8a | ||
|
|
92d2cb35c4 | ||
|
|
6ce3d540e8 | ||
|
|
c105d84b35 | ||
|
|
8b149fb52f | ||
|
|
3ea04c076c | ||
|
|
98475460bf | ||
|
|
68ee61a753 | ||
|
|
90ba9ef2b7 | ||
|
|
0ec114322e |
@@ -1,3 +1,8 @@
|
|||||||
|
---
|
||||||
|
name: v2rayNG程序问题
|
||||||
|
about: 创建一个报告来帮助我们改进
|
||||||
|
---
|
||||||
|
|
||||||
在提出问题前请先自行排除服务器端问题,同时也请通过搜索确认是否有人提出过相同问题。
|
在提出问题前请先自行排除服务器端问题,同时也请通过搜索确认是否有人提出过相同问题。
|
||||||
|
|
||||||
|
|
||||||
@@ -14,7 +19,8 @@
|
|||||||
|
|
||||||
### 日志信息
|
### 日志信息
|
||||||
<details>
|
<details>
|
||||||
通过 `adb logcat -s com.v2ray.ang GoLog V2rayConfigUtilGoLog Main` 获取日志。请自行删减日志中可能出现的敏感信息。
|
|
||||||
|
通过`adb logcat -s com.v2ray.ang GoLog V2rayConfigUtilGoLog Main`获取日志。请自行删减日志中可能出现的敏感信息。
|
||||||
|
|
||||||
如果问题可重现,建议先执行`adb logcat -c`清空系统日志再执行上述命令,再操作重现问题。
|
如果问题可重现,建议先执行`adb logcat -c`清空系统日志再执行上述命令,再操作重现问题。
|
||||||
```
|
```
|
||||||
5
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
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的问题,请至这个链接讨论。
|
||||||
12
.gitignore
vendored
12
.gitignore
vendored
@@ -1,11 +1,5 @@
|
|||||||
V2rayNG/app/src/main/res/layout/activity_inapp_buy.xml
|
|
||||||
V2rayNG/app/src/main/assets/geoip.dat
|
|
||||||
V2rayNG/app/src/main/assets/geosite.dat
|
|
||||||
V2rayNG/app/src/main/java/com/v2ray/ang/InappBuyActivity.java
|
|
||||||
V2rayNG/gradle/wrapper/gradle-wrapper.properties
|
|
||||||
V2rayNG/gradle/wrapper/gradle-wrapper.properties
|
|
||||||
*.dat
|
*.dat
|
||||||
*.jks
|
*.jks
|
||||||
V2rayNG/gradle/wrapper/gradle-wrapper.properties
|
V2rayNG/app/release/output.json
|
||||||
V2rayNG/gradle/wrapper/gradle-wrapper.properties
|
.idea/
|
||||||
V2rayNG/app/release/output.json
|
.gradle/
|
||||||
|
|||||||
@@ -1,26 +0,0 @@
|
|||||||
sudo: required
|
|
||||||
language: go
|
|
||||||
go:
|
|
||||||
- "1.12"
|
|
||||||
go_import_path: github.com/2dust/AndroidLibV2rayLite
|
|
||||||
git:
|
|
||||||
depth: 5
|
|
||||||
addons:
|
|
||||||
apt:
|
|
||||||
update: true
|
|
||||||
before_script:
|
|
||||||
- sudo ntpdate -u time.google.com
|
|
||||||
- date
|
|
||||||
- make all
|
|
||||||
- make downloadGoMobile
|
|
||||||
script:
|
|
||||||
- make BuildMobile
|
|
||||||
after_success:
|
|
||||||
deploy:
|
|
||||||
provider: releases
|
|
||||||
api_key: ${GH_TOKEN}
|
|
||||||
file:
|
|
||||||
- libv2ray.aar
|
|
||||||
skip_cleanup: true
|
|
||||||
on:
|
|
||||||
tags: true
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
package CoreI
|
|
||||||
|
|
||||||
import (
|
|
||||||
v2core "v2ray.com/core"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Status struct {
|
|
||||||
IsRunning bool
|
|
||||||
PackageName string
|
|
||||||
|
|
||||||
Vpoint v2core.Server
|
|
||||||
}
|
|
||||||
|
|
||||||
func CheckVersion() int {
|
|
||||||
return 20
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *Status) GetDataDir() string {
|
|
||||||
return v.PackageName
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *Status) GetApp(name string) string {
|
|
||||||
return v.PackageName + name
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *Status) GetTun2socksArgs(localDNS bool, enableIPv6 bool) (ret []string) {
|
|
||||||
ret = []string{"--netif-ipaddr",
|
|
||||||
"26.26.26.2",
|
|
||||||
"--netif-netmask",
|
|
||||||
"255.255.255.252",
|
|
||||||
"--socks-server-addr",
|
|
||||||
"127.0.0.1:10808",
|
|
||||||
"--tunmtu",
|
|
||||||
"1500",
|
|
||||||
"--loglevel",
|
|
||||||
"notice",
|
|
||||||
"--enable-udprelay",
|
|
||||||
"--sock-path",
|
|
||||||
v.GetDataDir() + "sock_path",
|
|
||||||
}
|
|
||||||
|
|
||||||
if enableIPv6 {
|
|
||||||
ret = append(ret, "--netif-ip6addr", "da26:2626::2")
|
|
||||||
}
|
|
||||||
|
|
||||||
if localDNS {
|
|
||||||
ret = append(ret, "--dnsgw", "127.0.0.1:10807")
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *Status) GetVPNSetupArg(localDNS bool, enableIPv6 bool) (ret string) {
|
|
||||||
ret = "m,1500 a,26.26.26.1,30 r,0.0.0.0,0"
|
|
||||||
|
|
||||||
if enableIPv6 {
|
|
||||||
ret += " a,da26:2626::1,126 r,::,0"
|
|
||||||
}
|
|
||||||
if localDNS {
|
|
||||||
ret += " d,26.26.26.2"
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
pb:
|
|
||||||
go get -u github.com/golang/protobuf/protoc-gen-go
|
|
||||||
@echo "pb Start"
|
|
||||||
asset:
|
|
||||||
bash gen_assets.sh download
|
|
||||||
mkdir assets
|
|
||||||
cp -v data/*.dat assets/
|
|
||||||
# cd assets;curl https://raw.githubusercontent.com/2dust/AndroidLibV2rayLite/master/data/geosite.dat > geosite.dat
|
|
||||||
# cd assets;curl https://raw.githubusercontent.com/2dust/AndroidLibV2rayLite/master/data/geoip.dat > geoip.dat
|
|
||||||
|
|
||||||
shippedBinary:
|
|
||||||
cd shippedBinarys; $(MAKE) shippedBinary
|
|
||||||
|
|
||||||
fetchDep:
|
|
||||||
-go get github.com/2dust/AndroidLibV2rayLite
|
|
||||||
go get github.com/2dust/AndroidLibV2rayLite
|
|
||||||
|
|
||||||
ANDROID_HOME=$(HOME)/android-sdk-linux
|
|
||||||
export ANDROID_HOME
|
|
||||||
PATH:=$(PATH):$(GOPATH)/bin
|
|
||||||
export PATH
|
|
||||||
downloadGoMobile:
|
|
||||||
go get golang.org/x/mobile/cmd/...
|
|
||||||
sudo apt-get install -qq libstdc++6:i386 lib32z1 expect
|
|
||||||
cd ~ ;curl -L https://raw.githubusercontent.com/2dust/AndroidLibV2rayLite/master/ubuntu-cli-install-android-sdk.sh | sudo bash - > /dev/null
|
|
||||||
ls ~
|
|
||||||
ls ~/android-sdk-linux/
|
|
||||||
gomobile init ;gomobile bind -v -tags json github.com/2dust/AndroidLibV2rayLite
|
|
||||||
|
|
||||||
BuildMobile:
|
|
||||||
@echo Stub
|
|
||||||
|
|
||||||
all: asset pb shippedBinary fetchDep
|
|
||||||
@echo DONE
|
|
||||||
@@ -1,87 +0,0 @@
|
|||||||
package Escort
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"os/exec"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"log"
|
|
||||||
|
|
||||||
"github.com/2dust/AndroidLibV2rayLite/CoreI"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (v *Escorting) EscortRun(proc string, pt []string, additionalEnv string, sendFd func() int) {
|
|
||||||
log.Println(proc, pt)
|
|
||||||
count := 0
|
|
||||||
for count <= 42 {
|
|
||||||
cmd := exec.Command(proc, pt...)
|
|
||||||
cmd.Stdout = os.Stdout
|
|
||||||
cmd.Stderr = os.Stderr
|
|
||||||
|
|
||||||
if len(additionalEnv) > 0 {
|
|
||||||
//additionalEnv := "FOO=bar"
|
|
||||||
newEnv := append(os.Environ(), additionalEnv)
|
|
||||||
cmd.Env = newEnv
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := cmd.Start(); err != nil {
|
|
||||||
log.Println("EscortRun cmd.Start err", err)
|
|
||||||
goto CMDERROR
|
|
||||||
}
|
|
||||||
|
|
||||||
if v.escortProcess == nil {
|
|
||||||
log.Println("EscortRun v.escortProcess nil")
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
*v.escortProcess = append(*v.escortProcess, cmd.Process)
|
|
||||||
log.Println("EscortRun Waiting....")
|
|
||||||
|
|
||||||
if count > 0 {
|
|
||||||
go func() {
|
|
||||||
time.Sleep(time.Second)
|
|
||||||
sendFd()
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := cmd.Wait(); err != nil {
|
|
||||||
log.Println("EscortRun cmd.Wait err:", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
CMDERROR:
|
|
||||||
if v.Status.IsRunning {
|
|
||||||
log.Println("EscortRun Unexpected Exit, Restart now.")
|
|
||||||
count++
|
|
||||||
} else {
|
|
||||||
log.Println("EscortRun Exit")
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *Escorting) EscortingUp() {
|
|
||||||
if v.escortProcess != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
v.escortProcess = new([](*os.Process))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *Escorting) EscortingDown() {
|
|
||||||
if v.escortProcess == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Println("EscortingDown() Killing all escorted process ")
|
|
||||||
for _, pr := range *v.escortProcess {
|
|
||||||
pr.Kill()
|
|
||||||
if _, err := pr.Wait(); err != nil {
|
|
||||||
log.Println("EscortingDown pr.Wait err:", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
v.escortProcess = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type Escorting struct {
|
|
||||||
escortProcess *[](*os.Process)
|
|
||||||
Status *CoreI.Status
|
|
||||||
}
|
|
||||||
@@ -1 +1,20 @@
|
|||||||
# AndroidLibV2rayLite
|
# 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)
|
||||||
|
|||||||
@@ -1,279 +0,0 @@
|
|||||||
package VPN
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
"net"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"golang.org/x/sys/unix"
|
|
||||||
v2net "v2ray.com/core/common/net"
|
|
||||||
v2internet "v2ray.com/core/transport/internet"
|
|
||||||
)
|
|
||||||
|
|
||||||
type protectSet interface {
|
|
||||||
Protect(int) int
|
|
||||||
}
|
|
||||||
|
|
||||||
type resolved struct {
|
|
||||||
domain string
|
|
||||||
IPs []net.IP
|
|
||||||
Port int
|
|
||||||
ipIdx uint8
|
|
||||||
ipLock sync.Mutex
|
|
||||||
lastSwitched time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
// NextIP switch to another resolved result.
|
|
||||||
// there still be race-condition here if multiple err concurently occured
|
|
||||||
// may cause idx keep switching,
|
|
||||||
// but that's an outside error can hardly handled here
|
|
||||||
func (r *resolved) NextIP() {
|
|
||||||
r.ipLock.Lock()
|
|
||||||
defer r.ipLock.Unlock()
|
|
||||||
|
|
||||||
if len(r.IPs) > 1 {
|
|
||||||
|
|
||||||
// throttle, don't switch too quickly
|
|
||||||
now := time.Now()
|
|
||||||
if now.Sub(r.lastSwitched) < time.Second*5 {
|
|
||||||
log.Println("switch too quickly")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
r.lastSwitched = now
|
|
||||||
r.ipIdx++
|
|
||||||
|
|
||||||
} else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if r.ipIdx >= uint8(len(r.IPs)) {
|
|
||||||
r.ipIdx = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
cur := r.currentIP()
|
|
||||||
log.Printf("switched to next IP: %s", cur)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *resolved) currentIP() net.IP {
|
|
||||||
if len(r.IPs) > 0 {
|
|
||||||
return r.IPs[r.ipIdx]
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewPreotectedDialer ...
|
|
||||||
func NewPreotectedDialer(p protectSet) *ProtectedDialer {
|
|
||||||
d := &ProtectedDialer{
|
|
||||||
// prefer native lookup on Android
|
|
||||||
resolver: &net.Resolver{PreferGo: false},
|
|
||||||
protectSet: p,
|
|
||||||
}
|
|
||||||
return d
|
|
||||||
}
|
|
||||||
|
|
||||||
// ProtectedDialer ...
|
|
||||||
type ProtectedDialer struct {
|
|
||||||
currentServer string
|
|
||||||
resolveChan chan struct{}
|
|
||||||
|
|
||||||
vServer *resolved
|
|
||||||
resolver *net.Resolver
|
|
||||||
|
|
||||||
protectSet
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *ProtectedDialer) IsVServerReady() bool {
|
|
||||||
return (d.vServer != nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *ProtectedDialer) PrepareResolveChan() {
|
|
||||||
d.resolveChan = make(chan struct{})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *ProtectedDialer) ResolveChan() <-chan struct{} {
|
|
||||||
return d.resolveChan
|
|
||||||
}
|
|
||||||
|
|
||||||
// simplicated version of golang: internetAddrList in src/net/ipsock.go
|
|
||||||
func (d *ProtectedDialer) lookupAddr(addr string) (*resolved, error) {
|
|
||||||
|
|
||||||
var (
|
|
||||||
err error
|
|
||||||
host, port string
|
|
||||||
portnum int
|
|
||||||
)
|
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
if host, port, err = net.SplitHostPort(addr); err != nil {
|
|
||||||
log.Printf("PrepareDomain SplitHostPort Err: %v", err)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if portnum, err = d.resolver.LookupPort(ctx, "tcp", port); err != nil {
|
|
||||||
log.Printf("PrepareDomain LookupPort Err: %v", err)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
addrs, err := d.resolver.LookupIPAddr(ctx, host)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if len(addrs) == 0 {
|
|
||||||
return nil, fmt.Errorf("domain %s Failed to resolve", addr)
|
|
||||||
}
|
|
||||||
|
|
||||||
IPs := make([]net.IP, len(addrs))
|
|
||||||
for i, ia := range addrs {
|
|
||||||
IPs[i] = ia.IP
|
|
||||||
}
|
|
||||||
|
|
||||||
rs := &resolved{
|
|
||||||
domain: host,
|
|
||||||
IPs: IPs,
|
|
||||||
Port: portnum,
|
|
||||||
}
|
|
||||||
|
|
||||||
return rs, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// PrepareDomain caches direct v2ray server host
|
|
||||||
func (d *ProtectedDialer) PrepareDomain(domainName string, closeCh <-chan struct{}) {
|
|
||||||
log.Printf("Preparing Domain: %s", domainName)
|
|
||||||
d.currentServer = domainName
|
|
||||||
|
|
||||||
defer close(d.resolveChan)
|
|
||||||
maxRetry := 10
|
|
||||||
for {
|
|
||||||
if maxRetry == 0 {
|
|
||||||
log.Println("PrepareDomain maxRetry reached. exiting.")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
resolved, err := d.lookupAddr(domainName)
|
|
||||||
if err != nil {
|
|
||||||
maxRetry--
|
|
||||||
log.Printf("PrepareDomain err: %v\n", err)
|
|
||||||
select {
|
|
||||||
case <-closeCh:
|
|
||||||
log.Printf("PrepareDomain exit due to v2ray closed")
|
|
||||||
return
|
|
||||||
case <-time.After(time.Second * 2):
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
d.vServer = resolved
|
|
||||||
log.Printf("Prepare Result:\n Domain: %s\n Port: %d\n IPs: %v\n",
|
|
||||||
resolved.domain, resolved.Port, resolved.IPs)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *ProtectedDialer) getFd(network v2net.Network) (fd int, err error) {
|
|
||||||
switch network {
|
|
||||||
case v2net.Network_TCP:
|
|
||||||
fd, err = unix.Socket(unix.AF_INET6, unix.SOCK_STREAM, unix.IPPROTO_TCP)
|
|
||||||
case v2net.Network_UDP:
|
|
||||||
fd, err = unix.Socket(unix.AF_INET6, unix.SOCK_DGRAM, unix.IPPROTO_UDP)
|
|
||||||
default:
|
|
||||||
err = fmt.Errorf("unknow network")
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Dial exported as the protected dial method
|
|
||||||
func (d *ProtectedDialer) Dial(ctx context.Context,
|
|
||||||
src v2net.Address, dest v2net.Destination, sockopt *v2internet.SocketConfig) (net.Conn, error) {
|
|
||||||
|
|
||||||
network := dest.Network.SystemString()
|
|
||||||
Address := dest.NetAddr()
|
|
||||||
|
|
||||||
// v2ray server address,
|
|
||||||
// try to connect fixed IP if multiple IP parsed from domain,
|
|
||||||
// and switch to next IP if error occurred
|
|
||||||
if strings.Compare(Address, d.currentServer) == 0 {
|
|
||||||
if d.vServer == nil {
|
|
||||||
log.Println("Dial pending prepare ...", Address)
|
|
||||||
<-d.resolveChan
|
|
||||||
|
|
||||||
// user may close connection during PrepareDomain,
|
|
||||||
// fast return release resources.
|
|
||||||
if d.vServer == nil {
|
|
||||||
return nil, fmt.Errorf("fail to prepare domain %s", d.currentServer)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fd, err := d.getFd(dest.Network)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
curIP := d.vServer.currentIP()
|
|
||||||
conn, err := d.fdConn(ctx, curIP, d.vServer.Port, fd)
|
|
||||||
if err != nil {
|
|
||||||
d.vServer.NextIP()
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
log.Printf("Using Prepared: %s", curIP)
|
|
||||||
return conn, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// v2ray connecting to "domestic" servers, no caching results
|
|
||||||
log.Printf("Not Using Prepared: %s,%s", network, Address)
|
|
||||||
resolved, err := d.lookupAddr(Address)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
fd, err := d.getFd(dest.Network)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// use the first resolved address.
|
|
||||||
// the result IP may vary, eg: IPv6 addrs comes first if client has ipv6 address
|
|
||||||
return d.fdConn(ctx, resolved.IPs[0], resolved.Port, fd)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *ProtectedDialer) fdConn(ctx context.Context, ip net.IP, port int, fd int) (net.Conn, error) {
|
|
||||||
|
|
||||||
defer unix.Close(fd)
|
|
||||||
|
|
||||||
// call android VPN service to "protect" the fd connecting straight out
|
|
||||||
d.Protect(fd)
|
|
||||||
|
|
||||||
sa := &unix.SockaddrInet6{
|
|
||||||
Port: port,
|
|
||||||
}
|
|
||||||
copy(sa.Addr[:], ip)
|
|
||||||
|
|
||||||
if err := unix.Connect(fd, sa); err != nil {
|
|
||||||
log.Printf("fdConn unix.Connect err, Close Fd: %d Err: %v", fd, err)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
file := os.NewFile(uintptr(fd), "Socket")
|
|
||||||
if file == nil {
|
|
||||||
// returned value will be nil if fd is not a valid file descriptor
|
|
||||||
return nil, errors.New("fdConn fd invalid")
|
|
||||||
}
|
|
||||||
|
|
||||||
defer file.Close()
|
|
||||||
//Closing conn does not affect file, and closing file does not affect conn.
|
|
||||||
conn, err := net.FileConn(file)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("fdConn FileConn Close Fd: %d Err: %v", fd, err)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return conn, nil
|
|
||||||
}
|
|
||||||
@@ -1,151 +0,0 @@
|
|||||||
package VPN
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bufio"
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"net"
|
|
||||||
"sync"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
v2net "v2ray.com/core/common/net"
|
|
||||||
)
|
|
||||||
|
|
||||||
type fakeSupportSet struct{}
|
|
||||||
|
|
||||||
func (f fakeSupportSet) Protect(int) int {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestProtectedDialer_PrepareDomain(t *testing.T) {
|
|
||||||
type args struct {
|
|
||||||
domainName string
|
|
||||||
}
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
args args
|
|
||||||
}{
|
|
||||||
// TODO: Add test cases.
|
|
||||||
{"", args{"baidu.com:80"}},
|
|
||||||
// {"", args{"cloudflare.com:443"}},
|
|
||||||
// {"", args{"apple.com:443"}},
|
|
||||||
// {"", args{"110.110.110.110:443"}},
|
|
||||||
// {"", args{"[2002:1234::1]:443"}},
|
|
||||||
}
|
|
||||||
d := NewPreotectedDialer(fakeSupportSet{})
|
|
||||||
for _, tt := range tests {
|
|
||||||
ch := make(chan struct{})
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
go d.PrepareDomain(tt.args.domainName, ch)
|
|
||||||
|
|
||||||
time.Sleep(time.Second)
|
|
||||||
go d.vServer.NextIP()
|
|
||||||
t.Log(d.vServer.currentIP())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
time.Sleep(time.Second)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestProtectedDialer_Dial(t *testing.T) {
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
wantErr bool
|
|
||||||
}{
|
|
||||||
// TODO: Add test cases.
|
|
||||||
{"baidu.com:80", false},
|
|
||||||
{"cloudflare.com:80", false},
|
|
||||||
{"172.16.192.11:80", true},
|
|
||||||
// {"172.16.192.10:80", true},
|
|
||||||
// {"[2fff:4322::1]:443", true},
|
|
||||||
// {"[fc00::1]:443", true},
|
|
||||||
}
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
ch := make(chan struct{})
|
|
||||||
|
|
||||||
d := NewPreotectedDialer(fakeSupportSet{})
|
|
||||||
d.currentServer = tt.name
|
|
||||||
|
|
||||||
go d.PrepareDomain(tt.name, ch)
|
|
||||||
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
|
|
||||||
dial := func() {
|
|
||||||
defer wg.Done()
|
|
||||||
dest, _ := v2net.ParseDestination("tcp:" + tt.name)
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
conn, err := d.Dial(ctx, nil, dest, nil)
|
|
||||||
if err != nil {
|
|
||||||
t.Log(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
_host, _, _ := net.SplitHostPort(tt.name)
|
|
||||||
fmt.Fprintf(conn, fmt.Sprintf("GET / HTTP/1.1\r\nHost: %s\r\n\r\n", _host))
|
|
||||||
status, err := bufio.NewReader(conn).ReadString('\n')
|
|
||||||
t.Logf("%#v, %#v\n", status, err)
|
|
||||||
conn.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
for n := 0; n < 3; n++ {
|
|
||||||
wg.Add(1)
|
|
||||||
go dial()
|
|
||||||
// time.Sleep(time.Millisecond * 10)
|
|
||||||
// d.pendingMap[tt.name] = make(chan struct{})
|
|
||||||
}
|
|
||||||
|
|
||||||
wg.Wait()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func Test_resolved_NextIP(t *testing.T) {
|
|
||||||
type fields struct {
|
|
||||||
domain string
|
|
||||||
IPs []net.IP
|
|
||||||
Port int
|
|
||||||
}
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
fields fields
|
|
||||||
}{
|
|
||||||
// TODO: Add test cases.
|
|
||||||
{"test1",
|
|
||||||
fields{
|
|
||||||
domain: "www.baidu.com",
|
|
||||||
IPs: []net.IP{
|
|
||||||
net.ParseIP("1.2.3.4"),
|
|
||||||
net.ParseIP("4.3.2.1"),
|
|
||||||
net.ParseIP("1234::1"),
|
|
||||||
net.ParseIP("4321::1"),
|
|
||||||
},
|
|
||||||
}},
|
|
||||||
}
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
r := &resolved{
|
|
||||||
domain: tt.fields.domain,
|
|
||||||
IPs: tt.fields.IPs,
|
|
||||||
Port: tt.fields.Port,
|
|
||||||
}
|
|
||||||
t.Logf("%v", r.IPs)
|
|
||||||
t.Logf("%v", r.currentIP())
|
|
||||||
r.NextIP()
|
|
||||||
t.Logf("%v", r.currentIP())
|
|
||||||
r.NextIP()
|
|
||||||
t.Logf("%v", r.currentIP())
|
|
||||||
r.NextIP()
|
|
||||||
t.Logf("%v", r.currentIP())
|
|
||||||
time.Sleep(3 * time.Second)
|
|
||||||
r.NextIP()
|
|
||||||
t.Logf("%v", r.currentIP())
|
|
||||||
time.Sleep(5 * time.Second)
|
|
||||||
r.NextIP()
|
|
||||||
t.Logf("%v", r.currentIP())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
set -o errexit
|
|
||||||
set -o pipefail
|
|
||||||
set -o nounset
|
|
||||||
|
|
||||||
# Set magic variables for current file & dir
|
|
||||||
__dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
||||||
__file="${__dir}/$(basename "${BASH_SOURCE[0]}")"
|
|
||||||
__base="$(basename ${__file} .sh)"
|
|
||||||
|
|
||||||
if [[ ! -d $NDK_HOME ]]; then
|
|
||||||
echo "Android NDK: NDK_HOME not found. please set env \$NDK_HOME"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
TMPDIR=$(mktemp -d)
|
|
||||||
clear_tmp () {
|
|
||||||
rm -rf $TMPDIR
|
|
||||||
}
|
|
||||||
|
|
||||||
trap 'echo -e "Aborted, error $? in command: $BASH_COMMAND"; trap ERR; clear_tmp; exit 1' ERR INT
|
|
||||||
install -m644 $__dir/tun2socks.mk $TMPDIR/
|
|
||||||
|
|
||||||
pushd $TMPDIR
|
|
||||||
git clone --depth=1 https://github.com/shadowsocks/badvpn.git
|
|
||||||
git clone --depth=1 https://github.com/shadowsocks/libancillary.git
|
|
||||||
$NDK_HOME/ndk-build \
|
|
||||||
NDK_PROJECT_PATH=. \
|
|
||||||
APP_BUILD_SCRIPT=./tun2socks.mk \
|
|
||||||
APP_ABI=all \
|
|
||||||
APP_PLATFORM=android-19 \
|
|
||||||
NDK_LIBS_OUT=$TMPDIR/libs \
|
|
||||||
NDK_OUT=$TMPDIR/tmp \
|
|
||||||
APP_SHORT_COMMANDS=false LOCAL_SHORT_COMMANDS=false -B -j4
|
|
||||||
|
|
||||||
install -v -m755 libs/armeabi-v7a/tun2socks $__dir/shippedBinarys/ArchDep/arm/
|
|
||||||
install -v -m755 libs/arm64-v8a/tun2socks $__dir/shippedBinarys/ArchDep/arm64/
|
|
||||||
install -v -m755 libs/x86/tun2socks $__dir/shippedBinarys/ArchDep/386/
|
|
||||||
install -v -m755 libs/x86_64/tun2socks $__dir/shippedBinarys/ArchDep/amd64/
|
|
||||||
popd
|
|
||||||
|
|
||||||
pushd $__dir/shippedBinarys
|
|
||||||
make clean && make shippedBinary
|
|
||||||
popd
|
|
||||||
|
|
||||||
rm -rf $TMPDIR
|
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
set -o errexit
|
|
||||||
set -o pipefail
|
|
||||||
set -o nounset
|
|
||||||
|
|
||||||
# Set magic variables for current file & dir
|
|
||||||
__dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
||||||
__file="${__dir}/$(basename "${BASH_SOURCE[0]}")"
|
|
||||||
__base="$(basename ${__file} .sh)"
|
|
||||||
|
|
||||||
|
|
||||||
DATADIR=${__dir}/data
|
|
||||||
|
|
||||||
compile_dat () {
|
|
||||||
local TMPDIR=$(mktemp -d)
|
|
||||||
|
|
||||||
trap 'echo -e "Aborted, error $? in command: $BASH_COMMAND"; rm -rf $TMPDIR; trap ERR; exit 1' ERR
|
|
||||||
|
|
||||||
local GEOSITE=${GOPATH}/src/github.com/v2ray/domain-list-community
|
|
||||||
if [[ -d ${GEOSITE} ]]; then
|
|
||||||
cd ${GEOSITE} && git pull
|
|
||||||
else
|
|
||||||
mkdir -p ${GEOSITE}
|
|
||||||
cd ${GEOSITE} && git clone https://github.com/v2ray/domain-list-community.git .
|
|
||||||
fi
|
|
||||||
go run main.go
|
|
||||||
|
|
||||||
if [[ -e dlc.dat ]]; then
|
|
||||||
rm -f $DATADIR/geosite.dat
|
|
||||||
mv dlc.dat $DATADIR/geosite.dat
|
|
||||||
echo "----------> geosite.dat updated."
|
|
||||||
else
|
|
||||||
echo "----------> geosite.dat failed to update."
|
|
||||||
fi
|
|
||||||
|
|
||||||
|
|
||||||
if [[ ! -x $GOPATH/bin/geoip ]]; then
|
|
||||||
go get -v -u github.com/v2ray/geoip
|
|
||||||
fi
|
|
||||||
|
|
||||||
cd $TMPDIR
|
|
||||||
curl -L -O http://geolite.maxmind.com/download/geoip/database/GeoLite2-Country-CSV.zip
|
|
||||||
unzip -q GeoLite2-Country-CSV.zip
|
|
||||||
mkdir geoip && find . -name '*.csv' -exec mv -t ./geoip {} +
|
|
||||||
$GOPATH/bin/geoip \
|
|
||||||
--country=./geoip/GeoLite2-Country-Locations-en.csv \
|
|
||||||
--ipv4=./geoip/GeoLite2-Country-Blocks-IPv4.csv \
|
|
||||||
--ipv6=./geoip/GeoLite2-Country-Blocks-IPv6.csv
|
|
||||||
|
|
||||||
if [[ -e geoip.dat ]]; then
|
|
||||||
rm -f $DATADIR/geoip.dat
|
|
||||||
mv ./geoip.dat $DATADIR/geoip.dat
|
|
||||||
echo "----------> geoip.dat updated."
|
|
||||||
else
|
|
||||||
echo "----------> geoip.dat failed to update."
|
|
||||||
fi
|
|
||||||
trap ERR
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
download_dat () {
|
|
||||||
wget -qO - https://api.github.com/repos/v2ray/geoip/releases/latest \
|
|
||||||
| grep browser_download_url | cut -d '"' -f 4 \
|
|
||||||
| wget -i - -O $DATADIR/geoip.dat
|
|
||||||
|
|
||||||
wget -qO - https://api.github.com/repos/v2ray/domain-list-community/releases/latest \
|
|
||||||
| grep browser_download_url | cut -d '"' -f 4 \
|
|
||||||
| wget -i - -O $DATADIR/geosite.dat
|
|
||||||
}
|
|
||||||
|
|
||||||
ACTION="${1:-}"
|
|
||||||
if [[ -z $ACTION ]]; then
|
|
||||||
ACTION=download
|
|
||||||
fi
|
|
||||||
|
|
||||||
case $ACTION in
|
|
||||||
"download") download_dat;;
|
|
||||||
"compile") compile_dat;;
|
|
||||||
esac
|
|
||||||
@@ -1,255 +0,0 @@
|
|||||||
package libv2ray
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/2dust/AndroidLibV2rayLite/CoreI"
|
|
||||||
"github.com/2dust/AndroidLibV2rayLite/Process/Escort"
|
|
||||||
"github.com/2dust/AndroidLibV2rayLite/VPN"
|
|
||||||
"github.com/2dust/AndroidLibV2rayLite/shippedBinarys"
|
|
||||||
mobasset "golang.org/x/mobile/asset"
|
|
||||||
|
|
||||||
v2core "v2ray.com/core"
|
|
||||||
v2filesystem "v2ray.com/core/common/platform/filesystem"
|
|
||||||
v2stats "v2ray.com/core/features/stats"
|
|
||||||
v2serial "v2ray.com/core/infra/conf/serial"
|
|
||||||
_ "v2ray.com/core/main/distro/all"
|
|
||||||
v2internet "v2ray.com/core/transport/internet"
|
|
||||||
|
|
||||||
v2applog "v2ray.com/core/app/log"
|
|
||||||
v2commlog "v2ray.com/core/common/log"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
v2Assert = "v2ray.location.asset"
|
|
||||||
assetperfix = "/dev/libv2rayfs0/asset"
|
|
||||||
)
|
|
||||||
|
|
||||||
/*V2RayPoint V2Ray Point Server
|
|
||||||
This is territory of Go, so no getter and setters!
|
|
||||||
*/
|
|
||||||
type V2RayPoint struct {
|
|
||||||
SupportSet V2RayVPNServiceSupportsSet
|
|
||||||
statsManager v2stats.Manager
|
|
||||||
|
|
||||||
dialer *VPN.ProtectedDialer
|
|
||||||
status *CoreI.Status
|
|
||||||
escorter *Escort.Escorting
|
|
||||||
v2rayOP *sync.Mutex
|
|
||||||
closeChan chan struct{}
|
|
||||||
|
|
||||||
PackageName string
|
|
||||||
DomainName string
|
|
||||||
ConfigureFileContent string
|
|
||||||
EnableLocalDNS bool
|
|
||||||
ForwardIpv6 bool
|
|
||||||
}
|
|
||||||
|
|
||||||
/*V2RayVPNServiceSupportsSet To support Android VPN mode*/
|
|
||||||
type V2RayVPNServiceSupportsSet interface {
|
|
||||||
Setup(Conf string) int
|
|
||||||
Prepare() int
|
|
||||||
Shutdown() int
|
|
||||||
Protect(int) int
|
|
||||||
OnEmitStatus(int, string) int
|
|
||||||
SendFd() int
|
|
||||||
}
|
|
||||||
|
|
||||||
/*RunLoop Run V2Ray main loop
|
|
||||||
*/
|
|
||||||
func (v *V2RayPoint) RunLoop() (err error) {
|
|
||||||
v.v2rayOP.Lock()
|
|
||||||
defer v.v2rayOP.Unlock()
|
|
||||||
//Construct Context
|
|
||||||
v.status.PackageName = v.PackageName
|
|
||||||
|
|
||||||
if !v.status.IsRunning {
|
|
||||||
v.closeChan = make(chan struct{})
|
|
||||||
v.dialer.PrepareResolveChan()
|
|
||||||
go v.dialer.PrepareDomain(v.DomainName, v.closeChan)
|
|
||||||
go func() {
|
|
||||||
select {
|
|
||||||
// wait until resolved
|
|
||||||
case <-v.dialer.ResolveChan():
|
|
||||||
// shutdown VPNService if server name can not reolved
|
|
||||||
if !v.dialer.IsVServerReady() {
|
|
||||||
log.Println("vServer cannot resolved, shutdown")
|
|
||||||
v.StopLoop()
|
|
||||||
v.SupportSet.Shutdown()
|
|
||||||
}
|
|
||||||
|
|
||||||
// stop waiting if manually closed
|
|
||||||
case <-v.closeChan:
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
err = v.pointloop()
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
/*StopLoop Stop V2Ray main loop
|
|
||||||
*/
|
|
||||||
func (v *V2RayPoint) StopLoop() (err error) {
|
|
||||||
v.v2rayOP.Lock()
|
|
||||||
defer v.v2rayOP.Unlock()
|
|
||||||
if v.status.IsRunning {
|
|
||||||
close(v.closeChan)
|
|
||||||
v.shutdownInit()
|
|
||||||
v.SupportSet.OnEmitStatus(0, "Closed")
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
//Delegate Funcation
|
|
||||||
func (v *V2RayPoint) GetIsRunning() bool {
|
|
||||||
return v.status.IsRunning
|
|
||||||
}
|
|
||||||
|
|
||||||
//Delegate Funcation
|
|
||||||
func (v V2RayPoint) QueryStats(tag string, direct string) int64 {
|
|
||||||
if v.statsManager == nil {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
counter := v.statsManager.GetCounter(fmt.Sprintf("inbound>>>%s>>>traffic>>>%s", tag, direct))
|
|
||||||
if counter == nil {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
return counter.Set(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *V2RayPoint) shutdownInit() {
|
|
||||||
v.status.IsRunning = false
|
|
||||||
v.status.Vpoint.Close()
|
|
||||||
v.status.Vpoint = nil
|
|
||||||
v.statsManager = nil
|
|
||||||
v.escorter.EscortingDown()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *V2RayPoint) pointloop() error {
|
|
||||||
if err := v.runTun2socks(); err != nil {
|
|
||||||
log.Println(err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Printf("EnableLocalDNS: %v\nForwardIpv6: %v\nDomainName: %s",
|
|
||||||
v.EnableLocalDNS,
|
|
||||||
v.ForwardIpv6,
|
|
||||||
v.DomainName)
|
|
||||||
|
|
||||||
log.Println("loading v2ray config")
|
|
||||||
config, err := v2serial.LoadJSONConfig(strings.NewReader(v.ConfigureFileContent))
|
|
||||||
if err != nil {
|
|
||||||
log.Println(err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Println("new v2ray core")
|
|
||||||
inst, err := v2core.New(config)
|
|
||||||
if err != nil {
|
|
||||||
log.Println(err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
v.status.Vpoint = inst
|
|
||||||
v.statsManager = inst.GetFeature(v2stats.ManagerType()).(v2stats.Manager)
|
|
||||||
|
|
||||||
log.Println("start v2ray core")
|
|
||||||
v.status.IsRunning = true
|
|
||||||
if err := v.status.Vpoint.Start(); err != nil {
|
|
||||||
v.status.IsRunning = false
|
|
||||||
log.Println(err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
v.SupportSet.Prepare()
|
|
||||||
v.SupportSet.Setup(v.status.GetVPNSetupArg(v.EnableLocalDNS, v.ForwardIpv6))
|
|
||||||
v.SupportSet.OnEmitStatus(0, "Running")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func initV2Env() {
|
|
||||||
if os.Getenv(v2Assert) != "" {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
//Initialize asset API, Since Raymond Will not let notify the asset location inside Process,
|
|
||||||
//We need to set location outside V2Ray
|
|
||||||
os.Setenv(v2Assert, assetperfix)
|
|
||||||
//Now we handle read
|
|
||||||
v2filesystem.NewFileReader = func(path string) (io.ReadCloser, error) {
|
|
||||||
if strings.HasPrefix(path, assetperfix) {
|
|
||||||
p := path[len(assetperfix)+1:]
|
|
||||||
//is it overridden?
|
|
||||||
//by, ok := overridedAssets[p]
|
|
||||||
//if ok {
|
|
||||||
// return os.Open(by)
|
|
||||||
//}
|
|
||||||
return mobasset.Open(p)
|
|
||||||
}
|
|
||||||
return os.Open(path)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//Delegate Funcation
|
|
||||||
func TestConfig(ConfigureFileContent string) error {
|
|
||||||
initV2Env()
|
|
||||||
_, err := v2serial.LoadJSONConfig(strings.NewReader(ConfigureFileContent))
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
/*NewV2RayPoint new V2RayPoint*/
|
|
||||||
func NewV2RayPoint(s V2RayVPNServiceSupportsSet) *V2RayPoint {
|
|
||||||
initV2Env()
|
|
||||||
|
|
||||||
// inject our own log writer
|
|
||||||
v2applog.RegisterHandlerCreator(v2applog.LogType_Console,
|
|
||||||
func(lt v2applog.LogType,
|
|
||||||
options v2applog.HandlerCreatorOptions) (v2commlog.Handler, error) {
|
|
||||||
return v2commlog.NewLogger(createStdoutLogWriter()), nil
|
|
||||||
})
|
|
||||||
|
|
||||||
dialer := VPN.NewPreotectedDialer(s)
|
|
||||||
v2internet.UseAlternativeSystemDialer(dialer)
|
|
||||||
status := &CoreI.Status{}
|
|
||||||
return &V2RayPoint{
|
|
||||||
SupportSet: s,
|
|
||||||
v2rayOP: new(sync.Mutex),
|
|
||||||
status: status,
|
|
||||||
dialer: dialer,
|
|
||||||
escorter: &Escort.Escorting{Status: status},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v V2RayPoint) runTun2socks() error {
|
|
||||||
shipb := shippedBinarys.FirstRun{Status: v.status}
|
|
||||||
if err := shipb.CheckAndExport(); err != nil {
|
|
||||||
log.Println(err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
v.escorter.EscortingUp()
|
|
||||||
go v.escorter.EscortRun(
|
|
||||||
v.status.GetApp("tun2socks"),
|
|
||||||
v.status.GetTun2socksArgs(v.EnableLocalDNS, v.ForwardIpv6), "",
|
|
||||||
v.SupportSet.SendFd)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
/*CheckVersion int
|
|
||||||
This func will return libv2ray binding version.
|
|
||||||
*/
|
|
||||||
func CheckVersion() int {
|
|
||||||
return CoreI.CheckVersion()
|
|
||||||
}
|
|
||||||
|
|
||||||
/*CheckVersionX string
|
|
||||||
This func will return libv2ray binding version and V2Ray version used.
|
|
||||||
*/
|
|
||||||
func CheckVersionX() string {
|
|
||||||
return fmt.Sprintf("Libv2rayLite V%d, Core V%s", CheckVersion(), v2core.Version())
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
package libv2ray
|
|
||||||
|
|
||||||
//go:generate make all
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
readme.txt
|
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,13 +0,0 @@
|
|||||||
Platdep=shippedBinary.386 shippedBinary.amd64 shippedBinary.arm64 shippedBinary.arm
|
|
||||||
|
|
||||||
shippedBinaryDep:
|
|
||||||
go get -u github.com/jteeuwen/go-bindata/...
|
|
||||||
|
|
||||||
shippedBinary.%:
|
|
||||||
go-bindata -nometadata -nomemcopy -pkg shippedBinarys -o ./binary_$*.go -tags $* ArchIndep/ ArchDep/$*/
|
|
||||||
|
|
||||||
shippedBinary:shippedBinaryDep $(Platdep)
|
|
||||||
@echo "Done"
|
|
||||||
|
|
||||||
clean:
|
|
||||||
-rm binary*
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
package shippedBinarys
|
|
||||||
|
|
||||||
import (
|
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"github.com/2dust/AndroidLibV2rayLite/CoreI"
|
|
||||||
)
|
|
||||||
|
|
||||||
type FirstRun struct {
|
|
||||||
Status *CoreI.Status
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *FirstRun) checkIfRcExist() error {
|
|
||||||
datadir := v.Status.GetDataDir()
|
|
||||||
if _, err := os.Stat(datadir + strconv.Itoa(CoreI.CheckVersion())); !os.IsNotExist(err) {
|
|
||||||
log.Println("file exists")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
IndepDir, err := AssetDir("ArchIndep")
|
|
||||||
log.Println(IndepDir)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
for _, fn := range IndepDir {
|
|
||||||
log.Println(datadir+"ArchIndep/"+fn)
|
|
||||||
|
|
||||||
err := RestoreAsset(datadir, "ArchIndep/"+fn)
|
|
||||||
log.Println(err)
|
|
||||||
|
|
||||||
//GrantPremission
|
|
||||||
os.Chmod(datadir+"ArchIndep/"+fn, 0700)
|
|
||||||
log.Println(os.Remove(datadir + fn))
|
|
||||||
log.Println(os.Symlink(datadir+"ArchIndep/"+fn, datadir + fn))
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
DepDir, err := AssetDir("ArchDep")
|
|
||||||
log.Println(DepDir)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
for _, fn := range DepDir {
|
|
||||||
DepDir2, err := AssetDir("ArchDep/" + fn)
|
|
||||||
log.Println("ArchDep/" + fn)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
for _, FND := range DepDir2 {
|
|
||||||
log.Println(datadir+"ArchDep/"+fn+"/"+FND)
|
|
||||||
|
|
||||||
RestoreAsset(datadir, "ArchDep/"+fn+"/"+FND)
|
|
||||||
os.Chmod(datadir+"ArchDep/"+fn+"/"+FND, 0700)
|
|
||||||
log.Println(os.Remove(datadir + FND))
|
|
||||||
log.Println(os.Symlink(datadir+"ArchDep/"+fn+"/"+FND, datadir+FND))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
s, _ := os.Create(datadir + strconv.Itoa(CoreI.CheckVersion()))
|
|
||||||
s.Close()
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *FirstRun) CheckAndExport() error {
|
|
||||||
return v.checkIfRcExist()
|
|
||||||
}
|
|
||||||
@@ -1,128 +0,0 @@
|
|||||||
# Copyright (C) 2009 The Android Open Source Project
|
|
||||||
#
|
|
||||||
# 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.
|
|
||||||
#
|
|
||||||
#
|
|
||||||
LOCAL_PATH := $(call my-dir)
|
|
||||||
ROOT_PATH := $(LOCAL_PATH)
|
|
||||||
|
|
||||||
########################################################
|
|
||||||
## libancillary
|
|
||||||
########################################################
|
|
||||||
|
|
||||||
include $(CLEAR_VARS)
|
|
||||||
|
|
||||||
ANCILLARY_SOURCE := fd_recv.c fd_send.c
|
|
||||||
|
|
||||||
LOCAL_MODULE := libancillary
|
|
||||||
#LOCAL_CFLAGS += -I$(LOCAL_PATH)/libancillary
|
|
||||||
LOCAL_C_INCLUDES := $(LOCAL_PATH)/libancillary
|
|
||||||
LOCAL_SRC_FILES := $(addprefix libancillary/, $(ANCILLARY_SOURCE))
|
|
||||||
|
|
||||||
include $(BUILD_STATIC_LIBRARY)
|
|
||||||
|
|
||||||
########################################################
|
|
||||||
## tun2socks
|
|
||||||
########################################################
|
|
||||||
|
|
||||||
include $(CLEAR_VARS)
|
|
||||||
|
|
||||||
LOCAL_CFLAGS := -std=gnu99
|
|
||||||
LOCAL_CFLAGS += -DBADVPN_THREADWORK_USE_PTHREAD -DBADVPN_LINUX -DBADVPN_BREACTOR_BADVPN -D_GNU_SOURCE
|
|
||||||
LOCAL_CFLAGS += -DBADVPN_USE_SIGNALFD -DBADVPN_USE_EPOLL
|
|
||||||
LOCAL_CFLAGS += -DBADVPN_LITTLE_ENDIAN -DBADVPN_THREAD_SAFE
|
|
||||||
LOCAL_CFLAGS += -DNDEBUG -DANDROID
|
|
||||||
LOCAL_CFLAGS += -I
|
|
||||||
|
|
||||||
LOCAL_STATIC_LIBRARIES := libancillary
|
|
||||||
|
|
||||||
LOCAL_C_INCLUDES := \
|
|
||||||
$(LOCAL_PATH)/badvpn/libancillary \
|
|
||||||
$(LOCAL_PATH)/badvpn/lwip/src/include/ipv4 \
|
|
||||||
$(LOCAL_PATH)/badvpn/lwip/src/include/ipv6 \
|
|
||||||
$(LOCAL_PATH)/badvpn/lwip/src/include \
|
|
||||||
$(LOCAL_PATH)/badvpn/lwip/custom \
|
|
||||||
$(LOCAL_PATH)/badvpn \
|
|
||||||
$(LOCAL_PATH)/libancillary
|
|
||||||
|
|
||||||
TUN2SOCKS_SOURCES := \
|
|
||||||
base/BLog_syslog.c \
|
|
||||||
system/BReactor_badvpn.c \
|
|
||||||
system/BSignal.c \
|
|
||||||
system/BConnection_common.c \
|
|
||||||
system/BConnection_unix.c \
|
|
||||||
system/BTime.c \
|
|
||||||
system/BUnixSignal.c \
|
|
||||||
system/BNetwork.c \
|
|
||||||
flow/StreamRecvInterface.c \
|
|
||||||
flow/PacketRecvInterface.c \
|
|
||||||
flow/PacketPassInterface.c \
|
|
||||||
flow/StreamPassInterface.c \
|
|
||||||
flow/SinglePacketBuffer.c \
|
|
||||||
flow/BufferWriter.c \
|
|
||||||
flow/PacketBuffer.c \
|
|
||||||
flow/PacketStreamSender.c \
|
|
||||||
flow/PacketPassConnector.c \
|
|
||||||
flow/PacketProtoFlow.c \
|
|
||||||
flow/PacketPassFairQueue.c \
|
|
||||||
flow/PacketProtoEncoder.c \
|
|
||||||
flow/PacketProtoDecoder.c \
|
|
||||||
socksclient/BSocksClient.c \
|
|
||||||
tuntap/BTap.c \
|
|
||||||
lwip/src/core/udp.c \
|
|
||||||
lwip/src/core/memp.c \
|
|
||||||
lwip/src/core/init.c \
|
|
||||||
lwip/src/core/pbuf.c \
|
|
||||||
lwip/src/core/tcp.c \
|
|
||||||
lwip/src/core/tcp_out.c \
|
|
||||||
lwip/src/core/netif.c \
|
|
||||||
lwip/src/core/def.c \
|
|
||||||
lwip/src/core/ip.c \
|
|
||||||
lwip/src/core/mem.c \
|
|
||||||
lwip/src/core/tcp_in.c \
|
|
||||||
lwip/src/core/stats.c \
|
|
||||||
lwip/src/core/inet_chksum.c \
|
|
||||||
lwip/src/core/timeouts.c \
|
|
||||||
lwip/src/core/ipv4/icmp.c \
|
|
||||||
lwip/src/core/ipv4/igmp.c \
|
|
||||||
lwip/src/core/ipv4/ip4_addr.c \
|
|
||||||
lwip/src/core/ipv4/ip4_frag.c \
|
|
||||||
lwip/src/core/ipv4/ip4.c \
|
|
||||||
lwip/src/core/ipv4/autoip.c \
|
|
||||||
lwip/src/core/ipv6/ethip6.c \
|
|
||||||
lwip/src/core/ipv6/inet6.c \
|
|
||||||
lwip/src/core/ipv6/ip6_addr.c \
|
|
||||||
lwip/src/core/ipv6/mld6.c \
|
|
||||||
lwip/src/core/ipv6/dhcp6.c \
|
|
||||||
lwip/src/core/ipv6/icmp6.c \
|
|
||||||
lwip/src/core/ipv6/ip6.c \
|
|
||||||
lwip/src/core/ipv6/ip6_frag.c \
|
|
||||||
lwip/src/core/ipv6/nd6.c \
|
|
||||||
lwip/custom/sys.c \
|
|
||||||
tun2socks/tun2socks.c \
|
|
||||||
base/DebugObject.c \
|
|
||||||
base/BLog.c \
|
|
||||||
base/BPending.c \
|
|
||||||
system/BDatagram_unix.c \
|
|
||||||
flowextra/PacketPassInactivityMonitor.c \
|
|
||||||
tun2socks/SocksUdpGwClient.c \
|
|
||||||
udpgw_client/UdpGwClient.c
|
|
||||||
|
|
||||||
LOCAL_MODULE := tun2socks
|
|
||||||
|
|
||||||
LOCAL_LDLIBS := -ldl -llog
|
|
||||||
|
|
||||||
LOCAL_SRC_FILES := $(addprefix badvpn/, $(TUN2SOCKS_SOURCES))
|
|
||||||
|
|
||||||
include $(BUILD_SYSTEM)/build-executable.mk
|
|
||||||
|
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# Thanks to https://gist.github.com/wenzhixin/43cf3ce909c24948c6e7
|
|
||||||
# Execute this script in your home directory. Lines 17 and 21 will prompt you for a y/n
|
|
||||||
|
|
||||||
# Install Oracle JDK 8
|
|
||||||
apt-get update
|
|
||||||
apt-get install -y openjdk-8-jdk
|
|
||||||
apt-get install -y unzip make expect # NDK stuff
|
|
||||||
|
|
||||||
# Get SDK tools (link from https://developer.android.com/studio/index.html#downloads)
|
|
||||||
wget -q https://dl.google.com/android/repository/sdk-tools-linux-3859397.zip
|
|
||||||
mkdir android-sdk-linux
|
|
||||||
unzip sdk*.zip -d android-sdk-linux
|
|
||||||
|
|
||||||
# Get NDK (https://developer.android.com/ndk/downloads/index.html)
|
|
||||||
# wget -q https://dl.google.com/android/repository/android-ndk-r15c-linux-x86_64.zip
|
|
||||||
# unzip android-ndk*.zip >> /dev/null
|
|
||||||
|
|
||||||
ACCEPT_LICENSES_URL=https://gist.githubusercontent.com/xiaokangwang/1489fd223d26581bfec92adb3cb0088e/raw/328eb6925099df5aae3e76790f8232f0fc378f8b/accept-licenses
|
|
||||||
|
|
||||||
ACCEPT_LICENSES_ITEM="android-sdk-license-bcbbd656|intel-android-sysimage-license-1ea702d1|android-sdk-license-2742d1c5"
|
|
||||||
|
|
||||||
# Let it update itself and install some stuff
|
|
||||||
cd android-sdk-linux/tools
|
|
||||||
|
|
||||||
curl -L -o accept-licenses $ACCEPT_LICENSES_URL
|
|
||||||
|
|
||||||
chmod +x accept-licenses
|
|
||||||
|
|
||||||
./accept-licenses "./android update sdk --use-sdk-wrapper --all --no-ui" $ACCEPT_LICENSES_ITEM >/dev/null
|
|
||||||
|
|
||||||
# Download every build-tools version that has ever existed
|
|
||||||
# This will save you time! Thank me later for this
|
|
||||||
|
|
||||||
#./accept-licenses "./android update sdk --use-sdk-wrapper --all --no-ui --filter 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27" $ACCEPT_LICENSES_ITEM
|
|
||||||
|
|
||||||
PACKAGE_PARSE_URL=https://gist.githubusercontent.com/xiaokangwang/06268fb23034ed94bc301880e862da09/raw/afd95cbbe2f8c1d9e7b0277b7c5ef39af756a6ee/parse.awk
|
|
||||||
|
|
||||||
reduceout=https://gist.githubusercontent.com/xiaokangwang/4684bdb5c3415b943f52aa4803386480/raw/b46dab1cc60f02c0d87f88f01e27157034218faa/out.awk
|
|
||||||
|
|
||||||
cd bin
|
|
||||||
|
|
||||||
curl -L -o parse.awk $PACKAGE_PARSE_URL
|
|
||||||
|
|
||||||
curl -L -o reduce.awk $reduceout
|
|
||||||
|
|
||||||
sudo apt-get install gawk
|
|
||||||
|
|
||||||
./sdkmanager --verbose --list |awk -f parse.awk > ~/package_to_install
|
|
||||||
|
|
||||||
readarray -t filenames < $HOME/package_to_install
|
|
||||||
|
|
||||||
cat $HOME/package_to_install
|
|
||||||
|
|
||||||
yes|./sdkmanager --verbose "${filenames[@]}" |awk -f reduce.awk
|
|
||||||
|
|
||||||
# If you need additional packages for your app, check available packages with:
|
|
||||||
# ./android list sdk --all
|
|
||||||
|
|
||||||
# install certain packages with:
|
|
||||||
# ./android update sdk --no-ui --all --filter 1,2,3,<...>,N
|
|
||||||
# where N is the number of the package in the list (see previous command)
|
|
||||||
|
|
||||||
./sdkmanager "ndk-bundle"
|
|
||||||
|
|
||||||
# Add the directory containing executables in PATH so that they can be found
|
|
||||||
echo 'export ANDROID_HOME=$HOME/android-sdk-linux' >> ~/.bashrc
|
|
||||||
echo 'export PATH=$PATH:$ANDROID_HOME/tools:$ANDROID_HOME/platform-tools' >> ~/.bashrc
|
|
||||||
# echo 'export NDK_HOME=$HOME/android-ndk-r15c' >> ~/.bashrc
|
|
||||||
# echo 'export ANDROID_NDK_HOME=$NDK_HOME' >> ~/.bashrc
|
|
||||||
|
|
||||||
|
|
||||||
source ~/.bashrc
|
|
||||||
|
|
||||||
# Make sure you can execute 32 bit executables if this is 64 bit machine, otherwise skip this
|
|
||||||
dpkg --add-architecture i386
|
|
||||||
apt-get update
|
|
||||||
apt-get install -y libc6:i386 libstdc++6:i386 zlib1g:i386
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
package libv2ray
|
|
||||||
|
|
||||||
// This struct creates our own log writer without datatime stamp
|
|
||||||
// As Android adds time stamps on each line
|
|
||||||
|
|
||||||
import (
|
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
v2commlog "v2ray.com/core/common/log"
|
|
||||||
)
|
|
||||||
|
|
||||||
type consoleLogWriter struct {
|
|
||||||
logger *log.Logger
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *consoleLogWriter) Write(s string) error {
|
|
||||||
w.logger.Print(s)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *consoleLogWriter) Close() error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// This logger won't print data/time stamps
|
|
||||||
func createStdoutLogWriter() v2commlog.WriterCreator {
|
|
||||||
return func() v2commlog.Writer {
|
|
||||||
return &consoleLogWriter{
|
|
||||||
logger: log.New(os.Stdout, "", 0)}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
31
README.md
31
README.md
@@ -1,5 +1,36 @@
|
|||||||
# v2rayNG
|
# 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">
|
<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" />
|
<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>
|
</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`
|
||||||
|
|||||||
1
V2rayNG/.gitignore
vendored
1
V2rayNG/.gitignore
vendored
@@ -7,3 +7,4 @@
|
|||||||
/captures
|
/captures
|
||||||
*.apk
|
*.apk
|
||||||
signing.properties
|
signing.properties
|
||||||
|
*.aar
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
apply plugin: 'com.android.application'
|
apply plugin: 'com.android.application'
|
||||||
apply plugin: 'kotlin-android'
|
apply plugin: 'kotlin-android'
|
||||||
apply plugin: 'kotlin-android-extensions'
|
|
||||||
|
Properties props = new Properties()
|
||||||
|
props.load(new FileInputStream(new File('local.properties')))
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion 28
|
compileSdkVersion Integer.parseInt("$compileSdkVer")
|
||||||
buildToolsVersion '28.0.3'
|
buildToolsVersion "$buildToolsVer"
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
targetCompatibility = "8"
|
targetCompatibility = "8"
|
||||||
@@ -13,25 +15,27 @@ android {
|
|||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId "com.v2ray.ang"
|
applicationId "com.v2ray.ang"
|
||||||
minSdkVersion 17
|
minSdkVersion 21
|
||||||
targetSdkVersion Integer.parseInt("$targetSdkVer")
|
targetSdkVersion Integer.parseInt("$targetSdkVer")
|
||||||
multiDexEnabled true
|
multiDexEnabled true
|
||||||
versionCode 212
|
versionCode 478
|
||||||
versionName "1.0.2"
|
versionName "1.7.22"
|
||||||
}
|
}
|
||||||
|
|
||||||
signingConfigs {
|
if (props["sign"]) {
|
||||||
release {
|
signingConfigs {
|
||||||
storeFile file("../key.jks")
|
release {
|
||||||
keyAlias 'ang'
|
storeFile file("../key.jks")
|
||||||
keyPassword '123456'
|
keyAlias 'ang'
|
||||||
storePassword '123456'
|
keyPassword '123456'
|
||||||
}
|
storePassword '123456'
|
||||||
debug {
|
}
|
||||||
storeFile file("../key.jks")
|
debug {
|
||||||
keyAlias 'ang'
|
storeFile file("../key.jks")
|
||||||
keyPassword '123456'
|
keyAlias 'ang'
|
||||||
storePassword '123456'
|
keyPassword '123456'
|
||||||
|
storePassword '123456'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,19 +44,32 @@ android {
|
|||||||
minifyEnabled false
|
minifyEnabled false
|
||||||
zipAlignEnabled false
|
zipAlignEnabled false
|
||||||
shrinkResources false
|
shrinkResources false
|
||||||
signingConfig signingConfigs.release
|
if (props["sign"]) {
|
||||||
|
signingConfig signingConfigs.release
|
||||||
|
}
|
||||||
|
ndk.abiFilters 'x86', 'x86_64', 'armeabi-v7a', 'arm64-v8a'
|
||||||
// proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
// proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||||
}
|
}
|
||||||
debug {
|
debug {
|
||||||
minifyEnabled false
|
minifyEnabled false
|
||||||
zipAlignEnabled false
|
zipAlignEnabled false
|
||||||
shrinkResources false
|
shrinkResources false
|
||||||
signingConfig signingConfigs.release
|
if (props["sign"]) {
|
||||||
|
signingConfig signingConfigs.release
|
||||||
|
}
|
||||||
|
ndk.abiFilters 'x86', 'x86_64', 'armeabi-v7a', 'arm64-v8a'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sourceSets {
|
sourceSets {
|
||||||
main.java.srcDirs += 'src/main/kotlin'
|
main {
|
||||||
|
jniLibs.srcDirs = ['libs']
|
||||||
|
java.srcDirs += 'src/main/kotlin'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
kotlinOptions {
|
||||||
|
jvmTarget = JavaVersion.VERSION_1_8
|
||||||
}
|
}
|
||||||
|
|
||||||
splits {
|
splits {
|
||||||
@@ -70,61 +87,65 @@ android {
|
|||||||
android.applicationVariants.all { variant ->
|
android.applicationVariants.all { variant ->
|
||||||
// assign different version code for each output
|
// assign different version code for each output
|
||||||
variant.outputs.each { output ->
|
variant.outputs.each { output ->
|
||||||
output.versionCodeOverride =
|
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) *
|
project.ext.versionCodes.get(output.getFilter(com.android.build.OutputFile.ABI), 0) *
|
||||||
1000000 + android.defaultConfig.versionCode
|
1000000 + android.defaultConfig.versionCode
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
buildFeatures {
|
||||||
|
viewBinding true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation fileTree(include: ['*.jar'], dir: 'libs')
|
implementation fileTree(dir: 'libs', include: ['*.aar', '*.jar'], exclude: [])
|
||||||
implementation 'com.android.support.constraint:constraint-layout:1.1.3'
|
testImplementation 'junit:junit:4.13.2'
|
||||||
testImplementation 'junit:junit:4.12'
|
|
||||||
implementation project(':dpreference')
|
// Androidx
|
||||||
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion"
|
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.kotlin:kotlin-reflect:$kotlinVersion"
|
||||||
// Android support library
|
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2"
|
||||||
implementation "com.android.support:support-v4:$supportLibVersion"
|
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2"
|
||||||
implementation "com.android.support:appcompat-v7:$supportLibVersion"
|
|
||||||
implementation "com.android.support:design:$supportLibVersion"
|
implementation 'com.tencent:mmkv-static:1.2.12'
|
||||||
implementation "com.android.support:cardview-v7:$supportLibVersion"
|
implementation 'com.google.code.gson:gson:2.8.9'
|
||||||
implementation "com.android.support:preference-v7:$supportLibVersion"
|
|
||||||
implementation "com.android.support:recyclerview-v7:$supportLibVersion"
|
|
||||||
implementation "com.android.support:multidex:1.0.3"
|
|
||||||
// DSL
|
|
||||||
implementation "org.jetbrains.anko:anko-sdk15:$ankoVersion"
|
|
||||||
implementation "org.jetbrains.anko:anko-support-v4:$ankoVersion"
|
|
||||||
implementation "org.jetbrains.anko:anko-appcompat-v7:$ankoVersion"
|
|
||||||
implementation "org.jetbrains.anko:anko-design:$ankoVersion"
|
|
||||||
implementation 'com.google.code.gson:gson:2.8.5'
|
|
||||||
implementation 'io.reactivex:rxjava:1.3.4'
|
implementation 'io.reactivex:rxjava:1.3.4'
|
||||||
implementation 'io.reactivex:rxandroid:1.2.1'
|
implementation 'io.reactivex:rxandroid:1.2.1'
|
||||||
implementation 'com.tbruyelle.rxpermissions:rxpermissions:0.9.4@aar'
|
implementation 'com.tbruyelle.rxpermissions:rxpermissions:0.9.4@aar'
|
||||||
implementation 'com.dinuscxj:recycleritemdecoration:1.0.0'
|
|
||||||
implementation 'io.reactivex:rxkotlin:0.60.0'
|
|
||||||
implementation 'me.dm7.barcodescanner:core:1.9.8'
|
implementation 'me.dm7.barcodescanner:core:1.9.8'
|
||||||
implementation 'me.dm7.barcodescanner:zxing:1.9.8'
|
implementation 'me.dm7.barcodescanner:zxing:1.9.8'
|
||||||
implementation 'com.github.jorgecastilloprz:fabprogresscircle:1.01@aar'
|
implementation 'com.github.jorgecastilloprz:fabprogresscircle:1.01@aar'
|
||||||
implementation 'com.beust:klaxon:3.0.1'
|
implementation 'me.drakeet.support:toastcompat:1.1.0'
|
||||||
implementation 'com.android.support:multidex:1.0.3'
|
implementation 'com.blacksquircle.ui:editorkit:2.1.1'
|
||||||
|
implementation 'com.blacksquircle.ui:language-base:2.1.1'
|
||||||
implementation(name: 'libv2ray', ext: 'aar')
|
implementation 'com.blacksquircle.ui:language-json:2.1.1'
|
||||||
//implementation(name: 'tun2socks', ext: 'aar')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
buildscript {
|
//buildscript {
|
||||||
repositories {
|
// repositories {
|
||||||
google()
|
// google()
|
||||||
jcenter()
|
// mavenCentral()
|
||||||
maven { url 'https://maven.google.com' }
|
// maven { url 'https://maven.google.com' }
|
||||||
}
|
// maven { url 'https://jitpack.io' }
|
||||||
dependencies {
|
// }
|
||||||
classpath "org.jetbrains.kotlin:kotlin-android-extensions:$kotlinVersion"
|
//}
|
||||||
}
|
|
||||||
}
|
|
||||||
repositories {
|
|
||||||
flatDir {
|
|
||||||
dirs 'libs'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
https://github.com/2dust/v2rayNG/tree/master/AndroidLibV2rayLite
|
|
||||||
BIN
V2rayNG/app/libs/arm64-v8a/libtun2socks.so
Normal file
BIN
V2rayNG/app/libs/arm64-v8a/libtun2socks.so
Normal file
Binary file not shown.
BIN
V2rayNG/app/libs/armeabi-v7a/libtun2socks.so
Normal file
BIN
V2rayNG/app/libs/armeabi-v7a/libtun2socks.so
Normal file
Binary file not shown.
Binary file not shown.
BIN
V2rayNG/app/libs/x86/libtun2socks.so
Normal file
BIN
V2rayNG/app/libs/x86/libtun2socks.so
Normal file
Binary file not shown.
BIN
V2rayNG/app/libs/x86_64/libtun2socks.so
Normal file
BIN
V2rayNG/app/libs/x86_64/libtun2socks.so
Normal file
Binary file not shown.
61
V2rayNG/app/proguard-rules.pro
vendored
61
V2rayNG/app/proguard-rules.pro
vendored
@@ -1,61 +0,0 @@
|
|||||||
# Add project specific ProGuard rules here.
|
|
||||||
# By default, the flags in this file are appended to flags specified
|
|
||||||
# in G:\android-sdk/tools/proguard/proguard-android.txt
|
|
||||||
# You can edit the include path and order by changing the proguardFiles
|
|
||||||
# directive in build.gradle.
|
|
||||||
#
|
|
||||||
# For more details, see
|
|
||||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
|
||||||
|
|
||||||
# Add any project specific keep options here:
|
|
||||||
|
|
||||||
# If your project uses WebView with JS, uncomment the following
|
|
||||||
# and specify the fully qualified class name to the JavaScript interface
|
|
||||||
# class:
|
|
||||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
|
||||||
# public *;
|
|
||||||
#}
|
|
||||||
|
|
||||||
# Gson uses generic type information stored in a class file when working with fields. Proguard
|
|
||||||
# removes such information by default, so configure it to keep all of it.
|
|
||||||
-keepattributes Signature
|
|
||||||
|
|
||||||
# For using GSON @Expose annotation
|
|
||||||
-keepattributes *Annotation*
|
|
||||||
|
|
||||||
# Gson specific classes
|
|
||||||
-keep class sun.misc.Unsafe { *; }
|
|
||||||
|
|
||||||
-dontwarn org.apache.commons.**
|
|
||||||
-keep class org.apache.commons.** { *;}
|
|
||||||
|
|
||||||
# Disable debug info output
|
|
||||||
-assumenosideeffects class android.util.Log {
|
|
||||||
public static boolean isLoggable(java.lang.String,int);
|
|
||||||
public static int v(...);
|
|
||||||
public static int i(...);
|
|
||||||
public static int w(...);
|
|
||||||
public static int d(...);
|
|
||||||
public static int e(...);
|
|
||||||
}
|
|
||||||
-assumenosideeffects class kotlin.jvm.internal.Intrinsics {
|
|
||||||
static void checkParameterIsNotNull(java.lang.Object, java.lang.String);
|
|
||||||
static void checkExpressionValueIsNotNull(java.lang.Object, java.lang.String);
|
|
||||||
static void throwUninitializedPropertyAccessException(java.lang.String);
|
|
||||||
}
|
|
||||||
|
|
||||||
-dontwarn org.jetbrains.anko.internals.**
|
|
||||||
-keep class org.jetbrains.anko.internals.** { *;}
|
|
||||||
|
|
||||||
-dontwarn rx.internal.util.unsafe.**
|
|
||||||
-keep class rx.internal.util.unsafe.** { *;}
|
|
||||||
|
|
||||||
-dontwarn app.dinus.**
|
|
||||||
-keep class app.dinus.** { *;}
|
|
||||||
|
|
||||||
-keepclassmembers class ** {
|
|
||||||
@com.hwangjr.rxbus.annotation.Subscribe public *;
|
|
||||||
@com.hwangjr.rxbus.annotation.Produce public *;
|
|
||||||
}
|
|
||||||
|
|
||||||
-keep class libv2ray.** { *;}
|
|
||||||
|
|||||||
@@ -1,72 +1,114 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
package="com.v2ray.ang">
|
package="com.v2ray.ang">
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
|
<supports-screens
|
||||||
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE"/>
|
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.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||||
<!-- <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" /> -->
|
<!-- <useapplications-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" /> -->
|
||||||
<uses-permission android:name="android.permission.CAMERA" />
|
<uses-permission android:name="android.permission.CAMERA" />
|
||||||
<uses-permission android:name="com.android.vending.BILLING" />
|
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
<!-- <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> -->
|
<!-- <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> -->
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:name=".AngApplication"
|
android:name=".AngApplication"
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/AppTheme">
|
android:theme="@style/AppThemeLight"
|
||||||
|
android:usesCleartextTraffic="true"
|
||||||
|
tools:targetApi="m">
|
||||||
<activity
|
<activity
|
||||||
|
android:exported="true"
|
||||||
android:name=".ui.MainActivity"
|
android:name=".ui.MainActivity"
|
||||||
android:theme="@style/AppTheme.NoActionBar"
|
|
||||||
android:launchMode="singleTask">
|
android:launchMode="singleTask">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
<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>
|
<intent-filter>
|
||||||
<action android:name="android.service.quicksettings.action.QS_TILE_PREFERENCES" />
|
<action android:name="android.service.quicksettings.action.QS_TILE_PREFERENCES" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="android.app.shortcuts"
|
android:name="android.app.shortcuts"
|
||||||
android:resource="@xml/shortcuts" />
|
android:resource="@xml/shortcuts" />
|
||||||
</activity>
|
</activity>
|
||||||
<activity
|
<activity
|
||||||
|
android:exported="false"
|
||||||
android:name=".ui.ServerActivity"
|
android:name=".ui.ServerActivity"
|
||||||
android:windowSoftInputMode="stateUnchanged" />
|
android:windowSoftInputMode="stateUnchanged" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.Server2Activity"
|
android:exported="false"
|
||||||
|
android:name=".ui.ServerCustomConfigActivity"
|
||||||
android:windowSoftInputMode="stateUnchanged" />
|
android:windowSoftInputMode="stateUnchanged" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.Server3Activity"
|
android:exported="false"
|
||||||
android:windowSoftInputMode="stateUnchanged" />
|
android:name=".ui.SettingsActivity" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.Server4Activity"
|
android:exported="false"
|
||||||
android:windowSoftInputMode="stateUnchanged" />
|
android:name=".ui.PerAppProxyActivity" />
|
||||||
<activity android:name=".ui.SettingsActivity" />
|
|
||||||
<activity android:name=".ui.PerAppProxyActivity" />
|
|
||||||
<activity android:name=".ui.ScannerActivity" />
|
|
||||||
<!-- <activity android:name=".InappBuyActivity" />-->
|
|
||||||
<activity android:name=".ui.LogcatActivity" />
|
|
||||||
<activity
|
<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:name=".ui.RoutingSettingsActivity"
|
||||||
android:windowSoftInputMode="stateUnchanged" />
|
android:windowSoftInputMode="stateUnchanged" />
|
||||||
<activity android:name=".ui.SubSettingActivity" />
|
<activity
|
||||||
|
android:exported="false"
|
||||||
|
android:name=".ui.SubSettingActivity" />
|
||||||
|
<activity
|
||||||
|
android:exported="false"
|
||||||
|
android:name=".ui.UserAssetActivity" />
|
||||||
|
|
||||||
<activity android:name=".ui.SubEditActivity" />
|
<activity
|
||||||
<activity android:name=".ui.ScScannerActivity" />
|
android:exported="false"
|
||||||
<activity android:name=".ui.ScSwitchActivity" />
|
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
|
<service
|
||||||
android:name=".service.V2RayVpnService"
|
android:name=".service.V2RayVpnService"
|
||||||
@@ -83,28 +125,45 @@
|
|||||||
android:value="true" />
|
android:value="true" />
|
||||||
</service>
|
</service>
|
||||||
|
|
||||||
<!--<receiver android:name=".receiver.WidgetProvider">-->
|
<service android:name=".service.V2RayProxyOnlyService"
|
||||||
<!--<meta-data-->
|
android:exported="false"
|
||||||
<!--android:name="android.appwidget.provider"-->
|
android:label="@string/app_name"
|
||||||
<!--android:resource="@xml/app_widget_provider" />-->
|
android:process=":RunSoLibV2RayDaemon">
|
||||||
|
</service>
|
||||||
|
|
||||||
<!--<intent-filter>-->
|
<service android:name=".service.V2RayTestService"
|
||||||
<!--<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />-->
|
android:exported="false"
|
||||||
<!--<action android:name="com.v2ray.ang.action.widget.click" />-->
|
android:process=":RunSoLibV2RayDaemon">
|
||||||
<!--</intent-filter>-->
|
</service>
|
||||||
<!--</receiver>-->
|
|
||||||
|
<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
|
<service
|
||||||
android:name=".service.QSTileService"
|
android:exported="true"
|
||||||
android:icon="@drawable/ic_v"
|
android:name=".service.QSTileService"
|
||||||
android:label="@string/app_tile_name"
|
android:icon="@drawable/ic_stat_name"
|
||||||
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
|
android:label="@string/app_tile_name"
|
||||||
|
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE"
|
||||||
|
android:process=":RunSoLibV2RayDaemon">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.service.quicksettings.action.QS_TILE" />
|
<action android:name="android.service.quicksettings.action.QS_TILE" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</service>
|
</service>
|
||||||
<!-- =====================Tasker===================== -->
|
<!-- =====================Tasker===================== -->
|
||||||
<activity
|
<activity
|
||||||
|
android:exported="true"
|
||||||
android:name=".ui.TaskerActivity"
|
android:name=".ui.TaskerActivity"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:label="@string/app_name">
|
android:label="@string/app_name">
|
||||||
@@ -113,7 +172,10 @@
|
|||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<receiver android:name=".receiver.TaskerReceiver">
|
<receiver
|
||||||
|
android:exported="true"
|
||||||
|
android:name=".receiver.TaskerReceiver"
|
||||||
|
android:process=":RunSoLibV2RayDaemon">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="com.twofortyfouram.locale.intent.action.FIRE_SETTING" />
|
<action android:name="com.twofortyfouram.locale.intent.action.FIRE_SETTING" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
@@ -122,4 +184,4 @@
|
|||||||
|
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|||||||
@@ -1,144 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2012 The Android Open Source Project
|
|
||||||
*
|
|
||||||
* 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.android.vending.billing;
|
|
||||||
|
|
||||||
import android.os.Bundle;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* InAppBillingService is the service that provides in-app billing version 3 and beyond.
|
|
||||||
* This service provides the following features:
|
|
||||||
* 1. Provides a new API to get details of in-app items published for the app including
|
|
||||||
* price, type, title and description.
|
|
||||||
* 2. The purchase flow is synchronous and purchase information is available immediately
|
|
||||||
* after it completes.
|
|
||||||
* 3. Purchase information of in-app purchases is maintained within the Google Play system
|
|
||||||
* till the purchase is consumed.
|
|
||||||
* 4. An API to consume a purchase of an inapp item. All purchases of one-time
|
|
||||||
* in-app items are consumable and thereafter can be purchased again.
|
|
||||||
* 5. An API to get current purchases of the user immediately. This will not contain any
|
|
||||||
* consumed purchases.
|
|
||||||
*
|
|
||||||
* All calls will give a response code with the following possible values
|
|
||||||
* RESULT_OK = 0 - success
|
|
||||||
* RESULT_USER_CANCELED = 1 - user pressed back or canceled a dialog
|
|
||||||
* RESULT_BILLING_UNAVAILABLE = 3 - this billing API version is not supported for the type requested
|
|
||||||
* RESULT_ITEM_UNAVAILABLE = 4 - requested SKU is not available for purchase
|
|
||||||
* RESULT_DEVELOPER_ERROR = 5 - invalid arguments provided to the API
|
|
||||||
* RESULT_ERROR = 6 - Fatal error during the API action
|
|
||||||
* RESULT_ITEM_ALREADY_OWNED = 7 - Failure to purchase since item is already owned
|
|
||||||
* RESULT_ITEM_NOT_OWNED = 8 - Failure to consume since item is not owned
|
|
||||||
*/
|
|
||||||
interface IInAppBillingService {
|
|
||||||
/**
|
|
||||||
* Checks support for the requested billing API version, package and in-app type.
|
|
||||||
* Minimum API version supported by this interface is 3.
|
|
||||||
* @param apiVersion the billing version which the app is using
|
|
||||||
* @param packageName the package name of the calling app
|
|
||||||
* @param type type of the in-app item being purchased "inapp" for one-time purchases
|
|
||||||
* and "subs" for subscription.
|
|
||||||
* @return RESULT_OK(0) on success, corresponding result code on failures
|
|
||||||
*/
|
|
||||||
int isBillingSupported(int apiVersion, String packageName, String type);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Provides details of a list of SKUs
|
|
||||||
* Given a list of SKUs of a valid type in the skusBundle, this returns a bundle
|
|
||||||
* with a list JSON strings containing the productId, price, title and description.
|
|
||||||
* This API can be called with a maximum of 20 SKUs.
|
|
||||||
* @param apiVersion billing API version that the Third-party is using
|
|
||||||
* @param packageName the package name of the calling app
|
|
||||||
* @param skusBundle bundle containing a StringArrayList of SKUs with key "ITEM_ID_LIST"
|
|
||||||
* @return Bundle containing the following key-value pairs
|
|
||||||
* "RESPONSE_CODE" with int value, RESULT_OK(0) if success, other response codes on
|
|
||||||
* failure as listed above.
|
|
||||||
* "DETAILS_LIST" with a StringArrayList containing purchase information
|
|
||||||
* in JSON format similar to:
|
|
||||||
* '{ "productId" : "exampleSku", "type" : "inapp", "price" : "$5.00",
|
|
||||||
* "title : "Example Title", "description" : "This is an example description" }'
|
|
||||||
*/
|
|
||||||
Bundle getSkuDetails(int apiVersion, String packageName, String type, in Bundle skusBundle);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a pending intent to launch the purchase flow for an in-app item by providing a SKU,
|
|
||||||
* the type, a unique purchase token and an optional developer payload.
|
|
||||||
* @param apiVersion billing API version that the app is using
|
|
||||||
* @param packageName package name of the calling app
|
|
||||||
* @param sku the SKU of the in-app item as published in the developer console
|
|
||||||
* @param type the type of the in-app item ("inapp" for one-time purchases
|
|
||||||
* and "subs" for subscription).
|
|
||||||
* @param developerPayload optional argument to be sent back with the purchase information
|
|
||||||
* @return Bundle containing the following key-value pairs
|
|
||||||
* "RESPONSE_CODE" with int value, RESULT_OK(0) if success, other response codes on
|
|
||||||
* failure as listed above.
|
|
||||||
* "BUY_INTENT" - PendingIntent to start the purchase flow
|
|
||||||
*
|
|
||||||
* The Pending intent should be launched with startIntentSenderForResult. When purchase flow
|
|
||||||
* has completed, the onActivityResult() will give a resultCode of OK or CANCELED.
|
|
||||||
* If the purchase is successful, the result data will contain the following key-value pairs
|
|
||||||
* "RESPONSE_CODE" with int value, RESULT_OK(0) if success, other response codes on
|
|
||||||
* failure as listed above.
|
|
||||||
* "INAPP_PURCHASE_DATA" - String in JSON format similar to
|
|
||||||
* '{"orderId":"12999763169054705758.1371079406387615",
|
|
||||||
* "packageName":"com.example.app",
|
|
||||||
* "productId":"exampleSku",
|
|
||||||
* "purchaseTime":1345678900000,
|
|
||||||
* "purchaseToken" : "122333444455555",
|
|
||||||
* "developerPayload":"example developer payload" }'
|
|
||||||
* "INAPP_DATA_SIGNATURE" - String containing the signature of the purchase data that
|
|
||||||
* was signed with the private key of the developer
|
|
||||||
* TODO: change this to app-specific keys.
|
|
||||||
*/
|
|
||||||
Bundle getBuyIntent(int apiVersion, String packageName, String sku, String type,
|
|
||||||
String developerPayload);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the current SKUs owned by the user of the type and package name specified along with
|
|
||||||
* purchase information and a signature of the data to be validated.
|
|
||||||
* This will return all SKUs that have been purchased in V3 and managed items purchased using
|
|
||||||
* V1 and V2 that have not been consumed.
|
|
||||||
* @param apiVersion billing API version that the app is using
|
|
||||||
* @param packageName package name of the calling app
|
|
||||||
* @param type the type of the in-app items being requested
|
|
||||||
* ("inapp" for one-time purchases and "subs" for subscription).
|
|
||||||
* @param continuationToken to be set as null for the first call, if the number of owned
|
|
||||||
* skus are too many, a continuationToken is returned in the response bundle.
|
|
||||||
* This method can be called again with the continuation token to get the next set of
|
|
||||||
* owned skus.
|
|
||||||
* @return Bundle containing the following key-value pairs
|
|
||||||
* "RESPONSE_CODE" with int value, RESULT_OK(0) if success, other response codes on
|
|
||||||
* failure as listed above.
|
|
||||||
* "INAPP_PURCHASE_ITEM_LIST" - StringArrayList containing the list of SKUs
|
|
||||||
* "INAPP_PURCHASE_DATA_LIST" - StringArrayList containing the purchase information
|
|
||||||
* "INAPP_DATA_SIGNATURE_LIST"- StringArrayList containing the signatures
|
|
||||||
* of the purchase information
|
|
||||||
* "INAPP_CONTINUATION_TOKEN" - String containing a continuation token for the
|
|
||||||
* next set of in-app purchases. Only set if the
|
|
||||||
* user has more owned skus than the current list.
|
|
||||||
*/
|
|
||||||
Bundle getPurchases(int apiVersion, String packageName, String type, String continuationToken);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Consume the last purchase of the given SKU. This will result in this item being removed
|
|
||||||
* from all subsequent responses to getPurchases() and allow re-purchase of this item.
|
|
||||||
* @param apiVersion billing API version that the app is using
|
|
||||||
* @param packageName package name of the calling app
|
|
||||||
* @param purchaseToken token in the purchase information JSON that identifies the purchase
|
|
||||||
* to be consumed
|
|
||||||
* @return 0 if consumption succeeded. Appropriate error values for failures.
|
|
||||||
*/
|
|
||||||
int consumePurchase(int apiVersion, String packageName, String purchaseToken);
|
|
||||||
}
|
|
||||||
1
V2rayNG/app/src/main/assets/custom_routing_block
Normal file
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
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
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,
|
||||||
@@ -1,196 +1,241 @@
|
|||||||
com.android.chrome
|
|
||||||
com.google.android.googlequicksearchbox
|
|
||||||
com.google.android.apps.photos
|
|
||||||
com.google.android.youtube
|
|
||||||
com.google.android.gm
|
|
||||||
com.google.android.apps.plus
|
|
||||||
com.android.vending
|
|
||||||
com.google.android.inputmethod.latin
|
|
||||||
com.google.android.apps.paidtasks
|
|
||||||
com.google.android.keep
|
|
||||||
com.google.android.gms.setup
|
|
||||||
com. google.android. apps.magazines
|
|
||||||
com.google.android.videos
|
|
||||||
com. google.android.gms
|
|
||||||
com.google.android.apps.books
|
|
||||||
com.google.android.music
|
|
||||||
com.google.android.play.games
|
|
||||||
com.google.android.gsf
|
|
||||||
com.google.android.gsf.login
|
|
||||||
com.app.pornhub
|
|
||||||
com.spotify.music
|
|
||||||
org.thunderdog.challegram
|
|
||||||
com.tumblr
|
|
||||||
com.twitter.android
|
|
||||||
com.xda.labs
|
|
||||||
com.kapp.youtube.final
|
|
||||||
com.google.android.ims
|
|
||||||
com.wire
|
|
||||||
mark.via.gp
|
|
||||||
com.downloader.video.tumblr
|
|
||||||
com.sololearn
|
|
||||||
com.cygames.shadowverse
|
|
||||||
com.felixfilip.scpae
|
|
||||||
amanita_design.samorost3.gp
|
amanita_design.samorost3.gp
|
||||||
com.devolver.reigns2
|
android
|
||||||
com.utopia.pxview
|
|
||||||
ch.protonmail.android
|
|
||||||
com.perol.asdpl.pixivez
|
|
||||||
com.pinterest
|
|
||||||
com.paypal.android.p2pmobile
|
|
||||||
com.arthurivanets.owly
|
|
||||||
com.rubenmayayo.reddit
|
|
||||||
com.rayark.cytus2
|
|
||||||
com.rayark.pluto
|
|
||||||
com.rayark.implosion
|
|
||||||
com.fireproofstudios.theroom4
|
|
||||||
com.netflix.mediaclient
|
|
||||||
com.instagram.android
|
|
||||||
com.google.android.apps.hangoutsdialer
|
|
||||||
com.google.android.talk
|
|
||||||
com.google.android.apps.plus
|
|
||||||
com.google.android.apps.pdfviewer
|
|
||||||
com.google.android.apps.magazines
|
|
||||||
com.google.android.apps.nbu.files
|
|
||||||
com.evernote
|
|
||||||
net.tsapps.appsales
|
|
||||||
com.google.android.apps.translate
|
|
||||||
com.google.ar.lens
|
|
||||||
com.google.android.apps.adm
|
|
||||||
com.google.android.apps.googleassistant
|
|
||||||
tw.com.gamer.android.activecenter
|
|
||||||
org.telegram.plus
|
|
||||||
com.brave.browser
|
|
||||||
com.breel.wallpapers18
|
|
||||||
com.teslacoilsw.launcher
|
|
||||||
com.lastpass.lpandroid
|
|
||||||
org.kustom.widget
|
|
||||||
com.fooview.android.fooview
|
|
||||||
com.google.android.apps.docs
|
|
||||||
com.google.android.apps.maps
|
|
||||||
com.facebook.services
|
|
||||||
com.facebook.system
|
|
||||||
com.facebook.katana
|
|
||||||
com.nianticlabs.ingress.prime.qa
|
|
||||||
com.vanced.android.youtube
|
|
||||||
com.nianticproject.ingress
|
|
||||||
com.quoord.tapatalkpro.activity
|
|
||||||
org.mozilla.firefox
|
|
||||||
com.reddit.frontpage
|
|
||||||
com.google.android.apps.fitness
|
|
||||||
au.com.shiftyjelly.pocketcasts
|
au.com.shiftyjelly.pocketcasts
|
||||||
com.google.android.gms
|
bbc.mobile.news.ww
|
||||||
com.android.providers.telephony
|
|
||||||
com.resilio.sync
|
|
||||||
com.google.android.apps.googlevoice
|
|
||||||
com.discord
|
|
||||||
com.cradle.iitc_mobile
|
|
||||||
be.mygod.vpnhotspot
|
be.mygod.vpnhotspot
|
||||||
|
ch.protonmail.android
|
||||||
|
co.wanqu.android
|
||||||
com.alphainventor.filemanager
|
com.alphainventor.filemanager
|
||||||
|
com.amazon.kindle
|
||||||
|
com.amazon.mshop.android.shopping
|
||||||
|
com.android.chrome
|
||||||
com.android.providers.downloads
|
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.apkpure.aegon
|
||||||
|
com.apkupdater
|
||||||
|
com.app.pornhub
|
||||||
|
com.arthurivanets.owly
|
||||||
|
com.asahi.tida.tablet
|
||||||
|
com.authy.authy
|
||||||
|
com.avmovie
|
||||||
com.ballistiq.artstation
|
com.ballistiq.artstation
|
||||||
|
com.binance.dev
|
||||||
com.bitly.app
|
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.canary
|
||||||
com.chrome.dev
|
com.chrome.dev
|
||||||
|
com.cl.newt66y
|
||||||
|
com.cradle.iitc_mobile
|
||||||
|
com.cygames.shadowverse
|
||||||
com.devhd.feedly
|
com.devhd.feedly
|
||||||
|
com.devolver.reigns2
|
||||||
|
com.discord
|
||||||
|
com.downloader.video.tumblr
|
||||||
|
com.driverbrowser
|
||||||
com.dropbox.android
|
com.dropbox.android
|
||||||
|
com.duolingo
|
||||||
|
com.duckduckgo.mobile.android
|
||||||
|
com.dv.adm
|
||||||
com.estrongs.android.pop
|
com.estrongs.android.pop
|
||||||
com.estrongs.android.pop.pro
|
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.fastaccess.github
|
||||||
|
com.felixfilip.scpae
|
||||||
|
com.fireproofstudios.theroom4
|
||||||
com.firstrowria.pushnotificationtester
|
com.firstrowria.pushnotificationtester
|
||||||
|
com.flyersoft.moonreaderp
|
||||||
|
com.fooview.android.fooview
|
||||||
com.fvd.eversync
|
com.fvd.eversync
|
||||||
|
com.gameloft.android.anmp.glofta8hm
|
||||||
|
com.gameloft.android.anmp.glofta9hm
|
||||||
com.gianlu.aria2app
|
com.gianlu.aria2app
|
||||||
com.github.yeriomin.yalpstore
|
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.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.instantapps.supervisor
|
||||||
|
com.google.android.keep
|
||||||
|
com.google.android.music
|
||||||
com.google.android.ogyoutube
|
com.google.android.ogyoutube
|
||||||
com.google.android.partnersetup
|
com.google.android.partnersetup
|
||||||
|
com.google.android.play.games
|
||||||
|
com.google.android.street
|
||||||
com.google.android.syncadapters.calendar
|
com.google.android.syncadapters.calendar
|
||||||
com.google.android.syncadapters.contacts
|
com.google.android.syncadapters.contacts
|
||||||
|
com.google.android.talk
|
||||||
com.google.android.tts
|
com.google.android.tts
|
||||||
|
com.google.android.videos
|
||||||
|
com.google.android.youtube
|
||||||
|
com.google.ar.lens
|
||||||
com.hochan.coldsoup
|
com.hochan.coldsoup
|
||||||
com.ifttt.ifttt
|
com.ifttt.ifttt
|
||||||
com.imgur.mobile
|
com.imgur.mobile
|
||||||
com.innologica.inoreader
|
com.innologica.inoreader
|
||||||
|
com.instagram.android
|
||||||
com.instapaper.android
|
com.instapaper.android
|
||||||
com.jarvanh.vpntether
|
com.jarvanh.vpntether
|
||||||
|
com.kapp.youtube.final
|
||||||
|
com.klinker.android.twitter_l
|
||||||
|
com.lastpass.lpandroid
|
||||||
|
com.linecorp.linelite
|
||||||
|
com.lingodeer
|
||||||
com.mediapods.tumbpods
|
com.mediapods.tumbpods
|
||||||
com.mgoogle.android.gms
|
com.mgoogle.android.gms
|
||||||
|
com.microsoft.emmx
|
||||||
com.microsoft.office.powerpoint
|
com.microsoft.office.powerpoint
|
||||||
|
com.microsoft.skydrive
|
||||||
com.mixplorer
|
com.mixplorer
|
||||||
com.msd.consumerchinese
|
com.msd.consumerchinese
|
||||||
com.msd.professionalchinese
|
com.msd.professionalchinese
|
||||||
com.mss2011c.sharehelper
|
com.mss2011c.sharehelper
|
||||||
|
com.netflix.mediaclient
|
||||||
com.newin.nplayer.pro
|
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.oasisfeng.island
|
||||||
|
com.ocnt.liveapp.hw
|
||||||
com.orekie.search
|
com.orekie.search
|
||||||
|
com.patreon.android
|
||||||
|
com.paypal.android.p2pmobile
|
||||||
|
com.perol.asdpl.pixivez
|
||||||
|
com.pinterest
|
||||||
|
com.popularapp.periodcalendar
|
||||||
com.popularapp.videodownloaderforinstagram
|
com.popularapp.videodownloaderforinstagram
|
||||||
com.pushbullet.android
|
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.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.slack
|
||||||
|
com.snaptube.premium
|
||||||
|
com.sololearn
|
||||||
|
com.sonelli.juicessh
|
||||||
|
com.spotify.music
|
||||||
com.tencent.huatuo
|
com.tencent.huatuo
|
||||||
com.termux
|
com.termux
|
||||||
|
com.teslacoilsw.launcher
|
||||||
|
com.theinitium.news
|
||||||
|
com.thomsonreuters.reuters
|
||||||
com.thunkable.android.hritvik00.freenom
|
com.thunkable.android.hritvik00.freenom
|
||||||
com.topjohnwu.magisk
|
com.topjohnwu.magisk
|
||||||
|
com.tripadvisor.tripadvisor
|
||||||
|
com.tumblr
|
||||||
|
com.twitter.android
|
||||||
com.u91porn
|
com.u91porn
|
||||||
com.u9porn
|
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.vimeo.android.videoapp
|
||||||
|
com.vivaldi.browser
|
||||||
|
com.vivaldi.browser.snapshot
|
||||||
|
com.vkontakte.android
|
||||||
|
com.whatsapp
|
||||||
|
com.wire
|
||||||
com.wuxiangai.refactor
|
com.wuxiangai.refactor
|
||||||
|
com.xda.labs
|
||||||
|
com.xvideos.app
|
||||||
com.yandex.browser
|
com.yandex.browser
|
||||||
|
com.yandex.browser.beta
|
||||||
|
com.yandex.browser.alpha
|
||||||
com.z28j.feel
|
com.z28j.feel
|
||||||
|
con.medium.reader
|
||||||
|
de.apkgrabber
|
||||||
de.robv.android.xposed.installer
|
de.robv.android.xposed.installer
|
||||||
dk.tacit.android.foldersync.full
|
dk.tacit.android.foldersync.full
|
||||||
es.rafalense.telegram.themes
|
es.rafalense.telegram.themes
|
||||||
es.rafalense.themes
|
es.rafalense.themes
|
||||||
flipboard.app
|
flipboard.app
|
||||||
|
fm.moon.app
|
||||||
|
fr.gouv.etalab.mastodon
|
||||||
github.tornaco.xposedmoduletest
|
github.tornaco.xposedmoduletest
|
||||||
|
idm.internet.download.manager
|
||||||
|
idm.internet.download.manager.plus
|
||||||
|
io.github.javiewer
|
||||||
|
io.github.skyhacker2.magnetsearch
|
||||||
io.va.exposed
|
io.va.exposed
|
||||||
|
it.mvilla.android.fenix2
|
||||||
|
jp.bokete.app.android
|
||||||
|
jp.naver.line.android
|
||||||
jp.pxv.android
|
jp.pxv.android
|
||||||
|
luo.speedometergpspro
|
||||||
|
mark.via.gp
|
||||||
me.tshine.easymark
|
me.tshine.easymark
|
||||||
net.teeha.android.url_shortener
|
net.teeha.android.url_shortener
|
||||||
|
net.tsapps.appsales
|
||||||
onion.fire
|
onion.fire
|
||||||
org.fdroid.fdroid
|
org.fdroid.fdroid
|
||||||
|
org.freedownloadmanager.fdm
|
||||||
|
org.kustom.widget
|
||||||
org.mozilla.fennec_aurora
|
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.schabi.newpipe
|
||||||
org.telegram.messenger
|
org.telegram.messenger
|
||||||
|
org.telegram.multi
|
||||||
|
org.telegram.plus
|
||||||
|
org.thunderdog.challegram
|
||||||
org.torproject.android
|
org.torproject.android
|
||||||
|
org.torproject.torbrowser_alpha
|
||||||
|
org.wikipedia
|
||||||
org.xbmc.kodi
|
org.xbmc.kodi
|
||||||
pl.zdunex25.updater
|
pl.zdunex25.updater
|
||||||
videodownloader.downloadvideo.downloader
|
|
||||||
com.quora.android
|
|
||||||
com.lingodeer
|
|
||||||
org.wikipedia
|
|
||||||
com.ninegag.android.app
|
|
||||||
com.duolingo
|
|
||||||
com.patreon.android
|
|
||||||
com.valvesoftware.android.steam.communimunity
|
|
||||||
co.wanqu.android
|
|
||||||
jp.bokete.app.android
|
|
||||||
com.vkontakte.android
|
|
||||||
com.amazon.mshop.android.shopping
|
|
||||||
com.ubisoft.dance.justdance2015companion
|
|
||||||
com.gameloft.android.anmp.glofta8hm
|
|
||||||
com.gameloft.android.anmp.glofta9hm
|
|
||||||
com.binance.dev
|
|
||||||
com.asahi.tida.tablet
|
|
||||||
com.theinitium.news
|
|
||||||
com.driverbrowser
|
|
||||||
com.thomsonreuters.reuters
|
|
||||||
com.nytimes.cn
|
|
||||||
com.android.providers.downloads.ui
|
|
||||||
com.avmovie
|
|
||||||
bbc.mobile.news.ww
|
|
||||||
org.mozilla.focus
|
|
||||||
io.github.javiewer
|
|
||||||
com.sonelli.juicessh
|
|
||||||
con.medium.reader
|
|
||||||
com.microsoft.skydrive
|
|
||||||
com.valvesoftware.android.steam.community
|
|
||||||
com.nintendo.zara
|
|
||||||
org.torproject.torbrowser_alpha
|
|
||||||
tv.twitch.android.app
|
tv.twitch.android.app
|
||||||
com.shanga.walli
|
tw.com.gamer.android.activecenter
|
||||||
com.whatsapp
|
videodownloader.downloadvideo.downloader
|
||||||
com.wire
|
uk.co.bbc.learningenglish
|
||||||
com.simplehabit.simplehabitapp
|
com.ted.android
|
||||||
|
|||||||
@@ -13,8 +13,8 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"system": {
|
"system": {
|
||||||
"statsInboundUplink": true,
|
"statsOutboundUplink": true,
|
||||||
"statsInboundDownlink": true
|
"statsOutboundDownlink": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"inbounds": [{
|
"inbounds": [{
|
||||||
@@ -54,7 +54,7 @@
|
|||||||
"users": [
|
"users": [
|
||||||
{
|
{
|
||||||
"id": "a3482e88-686a-4a58-8126-99c9df64b7bf",
|
"id": "a3482e88-686a-4a58-8126-99c9df64b7bf",
|
||||||
"alterId": 64,
|
"alterId": 0,
|
||||||
"security": "auto",
|
"security": "auto",
|
||||||
"level": 8
|
"level": 8
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 12 KiB |
@@ -16,8 +16,8 @@
|
|||||||
|
|
||||||
package com.v2ray.ang.helper;
|
package com.v2ray.ang.helper;
|
||||||
|
|
||||||
import android.support.v7.widget.RecyclerView;
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
import android.support.v7.widget.helper.ItemTouchHelper;
|
import androidx.recyclerview.widget.ItemTouchHelper;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Interface to listen for a move or dismissal event from a {@link ItemTouchHelper.Callback}.
|
* Interface to listen for a move or dismissal event from a {@link ItemTouchHelper.Callback}.
|
||||||
@@ -43,6 +43,8 @@ public interface ItemTouchHelperAdapter {
|
|||||||
boolean onItemMove(int fromPosition, int toPosition);
|
boolean onItemMove(int fromPosition, int toPosition);
|
||||||
|
|
||||||
|
|
||||||
|
void onItemMoveCompleted();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when an item has been dismissed by a swipe.<br/>
|
* Called when an item has been dismissed by a swipe.<br/>
|
||||||
* <br/>
|
* <br/>
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
|
|
||||||
package com.v2ray.ang.helper;
|
package com.v2ray.ang.helper;
|
||||||
|
|
||||||
import android.support.v7.widget.helper.ItemTouchHelper;
|
import androidx.recyclerview.widget.ItemTouchHelper;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Interface to notify an item ViewHolder of relevant callbacks from {@link
|
* Interface to notify an item ViewHolder of relevant callbacks from {@link
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
|
|
||||||
package com.v2ray.ang.helper;
|
package com.v2ray.ang.helper;
|
||||||
|
|
||||||
import android.support.v7.widget.RecyclerView;
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Listener for manual initiation of a drag.
|
* Listener for manual initiation of a drag.
|
||||||
|
|||||||
@@ -17,9 +17,11 @@
|
|||||||
package com.v2ray.ang.helper;
|
package com.v2ray.ang.helper;
|
||||||
|
|
||||||
import android.graphics.Canvas;
|
import android.graphics.Canvas;
|
||||||
import android.support.v7.widget.GridLayoutManager;
|
import androidx.recyclerview.widget.GridLayoutManager;
|
||||||
import android.support.v7.widget.RecyclerView;
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
import android.support.v7.widget.helper.ItemTouchHelper;
|
import androidx.recyclerview.widget.ItemTouchHelper;
|
||||||
|
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An implementation of {@link ItemTouchHelper.Callback} that enables basic drag & drop and
|
* An implementation of {@link ItemTouchHelper.Callback} that enables basic drag & drop and
|
||||||
@@ -52,7 +54,7 @@ public class SimpleItemTouchHelperCallback extends ItemTouchHelper.Callback {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
|
public int getMovementFlags(RecyclerView recyclerView, @NotNull RecyclerView.ViewHolder viewHolder) {
|
||||||
// Set movement flags based on the layout manager
|
// Set movement flags based on the layout manager
|
||||||
if (recyclerView.getLayoutManager() instanceof GridLayoutManager) {
|
if (recyclerView.getLayoutManager() instanceof GridLayoutManager) {
|
||||||
final int dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN | ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT;
|
final int dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN | ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT;
|
||||||
@@ -66,24 +68,25 @@ public class SimpleItemTouchHelperCallback extends ItemTouchHelper.Callback {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder source, RecyclerView.ViewHolder target) {
|
public boolean onMove(@NotNull RecyclerView recyclerView, RecyclerView.ViewHolder source, RecyclerView.ViewHolder target) {
|
||||||
if (source.getItemViewType() != target.getItemViewType()) {
|
if (source.getItemViewType() != target.getItemViewType()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Notify the adapter of the move
|
// Notify the adapter of the move
|
||||||
mAdapter.onItemMove(source.getAdapterPosition(), target.getAdapterPosition());
|
mAdapter.onItemMove(source.getBindingAdapterPosition(), target.getBindingAdapterPosition());
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onSwiped(RecyclerView.ViewHolder viewHolder, int i) {
|
public void onSwiped(RecyclerView.ViewHolder viewHolder, int i) {
|
||||||
// Notify the adapter of the dismissal
|
// Notify the adapter of the dismissal
|
||||||
mAdapter.onItemDismiss(viewHolder.getAdapterPosition());
|
mAdapter.onItemDismiss(viewHolder.getBindingAdapterPosition());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onChildDraw(Canvas c, RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, boolean isCurrentlyActive) {
|
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) {
|
if (actionState == ItemTouchHelper.ACTION_STATE_SWIPE) {
|
||||||
// Fade out the view as it is swiped out of the parent's bounds
|
// 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();
|
final float alpha = ALPHA_FULL - Math.abs(dX) / (float) viewHolder.itemView.getWidth();
|
||||||
@@ -109,9 +112,11 @@ public class SimpleItemTouchHelperCallback extends ItemTouchHelper.Callback {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void clearView(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
|
public void clearView(@NotNull RecyclerView recyclerView, @NotNull RecyclerView.ViewHolder viewHolder) {
|
||||||
super.clearView(recyclerView, viewHolder);
|
super.clearView(recyclerView, viewHolder);
|
||||||
|
|
||||||
|
mAdapter.onItemMoveCompleted();
|
||||||
|
|
||||||
viewHolder.itemView.setAlpha(ALPHA_FULL);
|
viewHolder.itemView.setAlpha(ALPHA_FULL);
|
||||||
|
|
||||||
if (viewHolder instanceof ItemTouchHelperViewHolder) {
|
if (viewHolder instanceof ItemTouchHelperViewHolder) {
|
||||||
|
|||||||
@@ -1,121 +0,0 @@
|
|||||||
package com.v2ray.ang.util;
|
|
||||||
|
|
||||||
import static android.content.Context.MODE_PRIVATE;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.res.AssetManager;
|
|
||||||
|
|
||||||
import java.io.BufferedReader;
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.FileOutputStream;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.io.InputStreamReader;
|
|
||||||
import java.io.OutputStream;
|
|
||||||
|
|
||||||
public class AssetsUtil {
|
|
||||||
public static boolean copyAssetFolder(AssetManager assetManager,
|
|
||||||
String fromAssetPath, String toPath) {
|
|
||||||
try {
|
|
||||||
String[] files = assetManager.list(fromAssetPath);
|
|
||||||
new File(toPath).mkdirs();
|
|
||||||
boolean res = true;
|
|
||||||
for (String file : files)
|
|
||||||
if (file.contains("."))
|
|
||||||
res &= copyAsset(assetManager,
|
|
||||||
fromAssetPath + "/" + file,
|
|
||||||
toPath + "/" + file);
|
|
||||||
else
|
|
||||||
res &= copyAssetFolder(assetManager,
|
|
||||||
fromAssetPath + "/" + file,
|
|
||||||
toPath + "/" + file);
|
|
||||||
return res;
|
|
||||||
} catch (Exception e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean copyAsset(AssetManager assetManager,
|
|
||||||
String fromAssetPath, String toPath) {
|
|
||||||
InputStream in = null;
|
|
||||||
OutputStream out = null;
|
|
||||||
try {
|
|
||||||
in = assetManager.open(fromAssetPath);
|
|
||||||
new File(toPath).createNewFile();
|
|
||||||
out = new FileOutputStream(toPath);
|
|
||||||
copyFile(in, out);
|
|
||||||
in.close();
|
|
||||||
return true;
|
|
||||||
} catch (Exception e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
return false;
|
|
||||||
} finally {
|
|
||||||
try {
|
|
||||||
if (out != null) {
|
|
||||||
out.close();
|
|
||||||
}
|
|
||||||
if (in != null) {
|
|
||||||
in.close();
|
|
||||||
}
|
|
||||||
} catch (IOException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String readTextFromAssets(AssetManager assetManager, String fileName) {
|
|
||||||
try {
|
|
||||||
InputStreamReader inputReader = new InputStreamReader(assetManager.open(fileName));
|
|
||||||
BufferedReader bufReader = new BufferedReader(inputReader);
|
|
||||||
String line;
|
|
||||||
String Result = "";
|
|
||||||
while ((line = bufReader.readLine()) != null)
|
|
||||||
Result += line;
|
|
||||||
return Result;
|
|
||||||
} catch (Exception e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String getAssetPath(Context context, String assetPath) {
|
|
||||||
InputStream in = null;
|
|
||||||
OutputStream out = null;
|
|
||||||
try {
|
|
||||||
context.deleteFile(assetPath);
|
|
||||||
|
|
||||||
in = context.getAssets().open(assetPath);
|
|
||||||
out = context.openFileOutput(assetPath, MODE_PRIVATE);
|
|
||||||
copyFile(in, out);
|
|
||||||
in.close();
|
|
||||||
|
|
||||||
String path = context.getFilesDir().toString();
|
|
||||||
return path + "/" + assetPath;
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
return "";
|
|
||||||
} finally {
|
|
||||||
try {
|
|
||||||
if (out != null) {
|
|
||||||
out.close();
|
|
||||||
}
|
|
||||||
if (in != null) {
|
|
||||||
in.close();
|
|
||||||
}
|
|
||||||
} catch (IOException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void copyFile(InputStream in, OutputStream out) throws IOException {
|
|
||||||
byte[] buffer = new byte[1024];
|
|
||||||
int read;
|
|
||||||
while ((read = in.read(buffer)) != -1) {
|
|
||||||
out.write(buffer, 0, read);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,570 +0,0 @@
|
|||||||
// Portions copyright 2002, Google, Inc.
|
|
||||||
//
|
|
||||||
// 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.util;
|
|
||||||
|
|
||||||
// This code was converted from code at http://iharder.sourceforge.net/base64/
|
|
||||||
// Lots of extraneous features were removed.
|
|
||||||
/* The original code said:
|
|
||||||
* <p>
|
|
||||||
* I am placing this code in the Public Domain. Do with it as you will.
|
|
||||||
* This software comes with no guarantees or warranties but with
|
|
||||||
* plenty of well-wishing instead!
|
|
||||||
* Please visit
|
|
||||||
* <a href="http://iharder.net/xmlizable">http://iharder.net/xmlizable</a>
|
|
||||||
* periodically to check for updates or to contribute improvements.
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @author Robert Harder
|
|
||||||
* @author rharder@usa.net
|
|
||||||
* @version 1.3
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Base64 converter class. This code is not a complete MIME encoder;
|
|
||||||
* it simply converts binary data to base64 data and back.
|
|
||||||
*
|
|
||||||
* <p>Note {@link CharBase64} is a GWT-compatible implementation of this
|
|
||||||
* class.
|
|
||||||
*/
|
|
||||||
public class Base64 {
|
|
||||||
/** Specify encoding (value is {@code true}). */
|
|
||||||
public final static boolean ENCODE = true;
|
|
||||||
|
|
||||||
/** Specify decoding (value is {@code false}). */
|
|
||||||
public final static boolean DECODE = false;
|
|
||||||
|
|
||||||
/** The equals sign (=) as a byte. */
|
|
||||||
private final static byte EQUALS_SIGN = (byte) '=';
|
|
||||||
|
|
||||||
/** The new line character (\n) as a byte. */
|
|
||||||
private final static byte NEW_LINE = (byte) '\n';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The 64 valid Base64 values.
|
|
||||||
*/
|
|
||||||
private final static byte[] ALPHABET =
|
|
||||||
{(byte) 'A', (byte) 'B', (byte) 'C', (byte) 'D', (byte) 'E', (byte) 'F',
|
|
||||||
(byte) 'G', (byte) 'H', (byte) 'I', (byte) 'J', (byte) 'K',
|
|
||||||
(byte) 'L', (byte) 'M', (byte) 'N', (byte) 'O', (byte) 'P',
|
|
||||||
(byte) 'Q', (byte) 'R', (byte) 'S', (byte) 'T', (byte) 'U',
|
|
||||||
(byte) 'V', (byte) 'W', (byte) 'X', (byte) 'Y', (byte) 'Z',
|
|
||||||
(byte) 'a', (byte) 'b', (byte) 'c', (byte) 'd', (byte) 'e',
|
|
||||||
(byte) 'f', (byte) 'g', (byte) 'h', (byte) 'i', (byte) 'j',
|
|
||||||
(byte) 'k', (byte) 'l', (byte) 'm', (byte) 'n', (byte) 'o',
|
|
||||||
(byte) 'p', (byte) 'q', (byte) 'r', (byte) 's', (byte) 't',
|
|
||||||
(byte) 'u', (byte) 'v', (byte) 'w', (byte) 'x', (byte) 'y',
|
|
||||||
(byte) 'z', (byte) '0', (byte) '1', (byte) '2', (byte) '3',
|
|
||||||
(byte) '4', (byte) '5', (byte) '6', (byte) '7', (byte) '8',
|
|
||||||
(byte) '9', (byte) '+', (byte) '/'};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The 64 valid web safe Base64 values.
|
|
||||||
*/
|
|
||||||
private final static byte[] WEBSAFE_ALPHABET =
|
|
||||||
{(byte) 'A', (byte) 'B', (byte) 'C', (byte) 'D', (byte) 'E', (byte) 'F',
|
|
||||||
(byte) 'G', (byte) 'H', (byte) 'I', (byte) 'J', (byte) 'K',
|
|
||||||
(byte) 'L', (byte) 'M', (byte) 'N', (byte) 'O', (byte) 'P',
|
|
||||||
(byte) 'Q', (byte) 'R', (byte) 'S', (byte) 'T', (byte) 'U',
|
|
||||||
(byte) 'V', (byte) 'W', (byte) 'X', (byte) 'Y', (byte) 'Z',
|
|
||||||
(byte) 'a', (byte) 'b', (byte) 'c', (byte) 'd', (byte) 'e',
|
|
||||||
(byte) 'f', (byte) 'g', (byte) 'h', (byte) 'i', (byte) 'j',
|
|
||||||
(byte) 'k', (byte) 'l', (byte) 'm', (byte) 'n', (byte) 'o',
|
|
||||||
(byte) 'p', (byte) 'q', (byte) 'r', (byte) 's', (byte) 't',
|
|
||||||
(byte) 'u', (byte) 'v', (byte) 'w', (byte) 'x', (byte) 'y',
|
|
||||||
(byte) 'z', (byte) '0', (byte) '1', (byte) '2', (byte) '3',
|
|
||||||
(byte) '4', (byte) '5', (byte) '6', (byte) '7', (byte) '8',
|
|
||||||
(byte) '9', (byte) '-', (byte) '_'};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Translates a Base64 value to either its 6-bit reconstruction value
|
|
||||||
* or a negative number indicating some other meaning.
|
|
||||||
**/
|
|
||||||
private final static byte[] DECODABET = {-9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 0 - 8
|
|
||||||
-5, -5, // Whitespace: Tab and Linefeed
|
|
||||||
-9, -9, // Decimal 11 - 12
|
|
||||||
-5, // Whitespace: Carriage Return
|
|
||||||
-9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 14 - 26
|
|
||||||
-9, -9, -9, -9, -9, // Decimal 27 - 31
|
|
||||||
-5, // Whitespace: Space
|
|
||||||
-9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 33 - 42
|
|
||||||
62, // Plus sign at decimal 43
|
|
||||||
-9, -9, -9, // Decimal 44 - 46
|
|
||||||
63, // Slash at decimal 47
|
|
||||||
52, 53, 54, 55, 56, 57, 58, 59, 60, 61, // Numbers zero through nine
|
|
||||||
-9, -9, -9, // Decimal 58 - 60
|
|
||||||
-1, // Equals sign at decimal 61
|
|
||||||
-9, -9, -9, // Decimal 62 - 64
|
|
||||||
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, // Letters 'A' through 'N'
|
|
||||||
14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, // Letters 'O' through 'Z'
|
|
||||||
-9, -9, -9, -9, -9, -9, // Decimal 91 - 96
|
|
||||||
26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, // Letters 'a' through 'm'
|
|
||||||
39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, // Letters 'n' through 'z'
|
|
||||||
-9, -9, -9, -9, -9 // Decimal 123 - 127
|
|
||||||
/* ,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 128 - 139
|
|
||||||
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 140 - 152
|
|
||||||
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 153 - 165
|
|
||||||
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 166 - 178
|
|
||||||
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 179 - 191
|
|
||||||
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 192 - 204
|
|
||||||
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 205 - 217
|
|
||||||
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 218 - 230
|
|
||||||
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 231 - 243
|
|
||||||
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9 // Decimal 244 - 255 */
|
|
||||||
};
|
|
||||||
|
|
||||||
/** The web safe decodabet */
|
|
||||||
private final static byte[] WEBSAFE_DECODABET =
|
|
||||||
{-9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 0 - 8
|
|
||||||
-5, -5, // Whitespace: Tab and Linefeed
|
|
||||||
-9, -9, // Decimal 11 - 12
|
|
||||||
-5, // Whitespace: Carriage Return
|
|
||||||
-9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 14 - 26
|
|
||||||
-9, -9, -9, -9, -9, // Decimal 27 - 31
|
|
||||||
-5, // Whitespace: Space
|
|
||||||
-9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 33 - 44
|
|
||||||
62, // Dash '-' sign at decimal 45
|
|
||||||
-9, -9, // Decimal 46-47
|
|
||||||
52, 53, 54, 55, 56, 57, 58, 59, 60, 61, // Numbers zero through nine
|
|
||||||
-9, -9, -9, // Decimal 58 - 60
|
|
||||||
-1, // Equals sign at decimal 61
|
|
||||||
-9, -9, -9, // Decimal 62 - 64
|
|
||||||
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, // Letters 'A' through 'N'
|
|
||||||
14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, // Letters 'O' through 'Z'
|
|
||||||
-9, -9, -9, -9, // Decimal 91-94
|
|
||||||
63, // Underscore '_' at decimal 95
|
|
||||||
-9, // Decimal 96
|
|
||||||
26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, // Letters 'a' through 'm'
|
|
||||||
39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, // Letters 'n' through 'z'
|
|
||||||
-9, -9, -9, -9, -9 // Decimal 123 - 127
|
|
||||||
/* ,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 128 - 139
|
|
||||||
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 140 - 152
|
|
||||||
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 153 - 165
|
|
||||||
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 166 - 178
|
|
||||||
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 179 - 191
|
|
||||||
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 192 - 204
|
|
||||||
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 205 - 217
|
|
||||||
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 218 - 230
|
|
||||||
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 231 - 243
|
|
||||||
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9 // Decimal 244 - 255 */
|
|
||||||
};
|
|
||||||
|
|
||||||
// Indicates white space in encoding
|
|
||||||
private final static byte WHITE_SPACE_ENC = -5;
|
|
||||||
// Indicates equals sign in encoding
|
|
||||||
private final static byte EQUALS_SIGN_ENC = -1;
|
|
||||||
|
|
||||||
/** Defeats instantiation. */
|
|
||||||
private Base64() {
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ******** E N C O D I N G M E T H O D S ******** */
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Encodes up to three bytes of the array <var>source</var>
|
|
||||||
* and writes the resulting four Base64 bytes to <var>destination</var>.
|
|
||||||
* The source and destination arrays can be manipulated
|
|
||||||
* anywhere along their length by specifying
|
|
||||||
* <var>srcOffset</var> and <var>destOffset</var>.
|
|
||||||
* This method does not check to make sure your arrays
|
|
||||||
* are large enough to accommodate <var>srcOffset</var> + 3 for
|
|
||||||
* the <var>source</var> array or <var>destOffset</var> + 4 for
|
|
||||||
* the <var>destination</var> array.
|
|
||||||
* The actual number of significant bytes in your array is
|
|
||||||
* given by <var>numSigBytes</var>.
|
|
||||||
*
|
|
||||||
* @param source the array to convert
|
|
||||||
* @param srcOffset the index where conversion begins
|
|
||||||
* @param numSigBytes the number of significant bytes in your array
|
|
||||||
* @param destination the array to hold the conversion
|
|
||||||
* @param destOffset the index where output will be put
|
|
||||||
* @param alphabet is the encoding alphabet
|
|
||||||
* @return the <var>destination</var> array
|
|
||||||
* @since 1.3
|
|
||||||
*/
|
|
||||||
private static byte[] encode3to4(byte[] source, int srcOffset,
|
|
||||||
int numSigBytes, byte[] destination, int destOffset, byte[] alphabet) {
|
|
||||||
// 1 2 3
|
|
||||||
// 01234567890123456789012345678901 Bit position
|
|
||||||
// --------000000001111111122222222 Array position from threeBytes
|
|
||||||
// --------| || || || | Six bit groups to index alphabet
|
|
||||||
// >>18 >>12 >> 6 >> 0 Right shift necessary
|
|
||||||
// 0x3f 0x3f 0x3f Additional AND
|
|
||||||
|
|
||||||
// Create buffer with zero-padding if there are only one or two
|
|
||||||
// significant bytes passed in the array.
|
|
||||||
// We have to shift left 24 in order to flush out the 1's that appear
|
|
||||||
// when Java treats a value as negative that is cast from a byte to an int.
|
|
||||||
int inBuff =
|
|
||||||
(numSigBytes > 0 ? ((source[srcOffset] << 24) >>> 8) : 0)
|
|
||||||
| (numSigBytes > 1 ? ((source[srcOffset + 1] << 24) >>> 16) : 0)
|
|
||||||
| (numSigBytes > 2 ? ((source[srcOffset + 2] << 24) >>> 24) : 0);
|
|
||||||
|
|
||||||
switch (numSigBytes) {
|
|
||||||
case 3:
|
|
||||||
destination[destOffset] = alphabet[(inBuff >>> 18)];
|
|
||||||
destination[destOffset + 1] = alphabet[(inBuff >>> 12) & 0x3f];
|
|
||||||
destination[destOffset + 2] = alphabet[(inBuff >>> 6) & 0x3f];
|
|
||||||
destination[destOffset + 3] = alphabet[(inBuff) & 0x3f];
|
|
||||||
return destination;
|
|
||||||
case 2:
|
|
||||||
destination[destOffset] = alphabet[(inBuff >>> 18)];
|
|
||||||
destination[destOffset + 1] = alphabet[(inBuff >>> 12) & 0x3f];
|
|
||||||
destination[destOffset + 2] = alphabet[(inBuff >>> 6) & 0x3f];
|
|
||||||
destination[destOffset + 3] = EQUALS_SIGN;
|
|
||||||
return destination;
|
|
||||||
case 1:
|
|
||||||
destination[destOffset] = alphabet[(inBuff >>> 18)];
|
|
||||||
destination[destOffset + 1] = alphabet[(inBuff >>> 12) & 0x3f];
|
|
||||||
destination[destOffset + 2] = EQUALS_SIGN;
|
|
||||||
destination[destOffset + 3] = EQUALS_SIGN;
|
|
||||||
return destination;
|
|
||||||
default:
|
|
||||||
return destination;
|
|
||||||
} // end switch
|
|
||||||
} // end encode3to4
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Encodes a byte array into Base64 notation.
|
|
||||||
* Equivalent to calling
|
|
||||||
* {@code encodeBytes(source, 0, source.length)}
|
|
||||||
*
|
|
||||||
* @param source The data to convert
|
|
||||||
* @since 1.4
|
|
||||||
*/
|
|
||||||
public static String encode(byte[] source) {
|
|
||||||
return encode(source, 0, source.length, ALPHABET, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Encodes a byte array into web safe Base64 notation.
|
|
||||||
*
|
|
||||||
* @param source The data to convert
|
|
||||||
* @param doPadding is {@code true} to pad result with '=' chars
|
|
||||||
* if it does not fall on 3 byte boundaries
|
|
||||||
*/
|
|
||||||
public static String encodeWebSafe(byte[] source, boolean doPadding) {
|
|
||||||
return encode(source, 0, source.length, WEBSAFE_ALPHABET, doPadding);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Encodes a byte array into Base64 notation.
|
|
||||||
*
|
|
||||||
* @param source the data to convert
|
|
||||||
* @param off offset in array where conversion should begin
|
|
||||||
* @param len length of data to convert
|
|
||||||
* @param alphabet the encoding alphabet
|
|
||||||
* @param doPadding is {@code true} to pad result with '=' chars
|
|
||||||
* if it does not fall on 3 byte boundaries
|
|
||||||
* @since 1.4
|
|
||||||
*/
|
|
||||||
public static String encode(byte[] source, int off, int len, byte[] alphabet,
|
|
||||||
boolean doPadding) {
|
|
||||||
byte[] outBuff = encode(source, off, len, alphabet, Integer.MAX_VALUE);
|
|
||||||
int outLen = outBuff.length;
|
|
||||||
|
|
||||||
// If doPadding is false, set length to truncate '='
|
|
||||||
// padding characters
|
|
||||||
while (doPadding == false && outLen > 0) {
|
|
||||||
if (outBuff[outLen - 1] != '=') {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
outLen -= 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return new String(outBuff, 0, outLen);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Encodes a byte array into Base64 notation.
|
|
||||||
*
|
|
||||||
* @param source the data to convert
|
|
||||||
* @param off offset in array where conversion should begin
|
|
||||||
* @param len length of data to convert
|
|
||||||
* @param alphabet is the encoding alphabet
|
|
||||||
* @param maxLineLength maximum length of one line.
|
|
||||||
* @return the BASE64-encoded byte array
|
|
||||||
*/
|
|
||||||
public static byte[] encode(byte[] source, int off, int len, byte[] alphabet,
|
|
||||||
int maxLineLength) {
|
|
||||||
int lenDiv3 = (len + 2) / 3; // ceil(len / 3)
|
|
||||||
int len43 = lenDiv3 * 4;
|
|
||||||
byte[] outBuff = new byte[len43 // Main 4:3
|
|
||||||
+ (len43 / maxLineLength)]; // New lines
|
|
||||||
|
|
||||||
int d = 0;
|
|
||||||
int e = 0;
|
|
||||||
int len2 = len - 2;
|
|
||||||
int lineLength = 0;
|
|
||||||
for (; d < len2; d += 3, e += 4) {
|
|
||||||
|
|
||||||
// The following block of code is the same as
|
|
||||||
// encode3to4( source, d + off, 3, outBuff, e, alphabet );
|
|
||||||
// but inlined for faster encoding (~20% improvement)
|
|
||||||
int inBuff =
|
|
||||||
((source[d + off] << 24) >>> 8)
|
|
||||||
| ((source[d + 1 + off] << 24) >>> 16)
|
|
||||||
| ((source[d + 2 + off] << 24) >>> 24);
|
|
||||||
outBuff[e] = alphabet[(inBuff >>> 18)];
|
|
||||||
outBuff[e + 1] = alphabet[(inBuff >>> 12) & 0x3f];
|
|
||||||
outBuff[e + 2] = alphabet[(inBuff >>> 6) & 0x3f];
|
|
||||||
outBuff[e + 3] = alphabet[(inBuff) & 0x3f];
|
|
||||||
|
|
||||||
lineLength += 4;
|
|
||||||
if (lineLength == maxLineLength) {
|
|
||||||
outBuff[e + 4] = NEW_LINE;
|
|
||||||
e++;
|
|
||||||
lineLength = 0;
|
|
||||||
} // end if: end of line
|
|
||||||
} // end for: each piece of array
|
|
||||||
|
|
||||||
if (d < len) {
|
|
||||||
encode3to4(source, d + off, len - d, outBuff, e, alphabet);
|
|
||||||
|
|
||||||
lineLength += 4;
|
|
||||||
if (lineLength == maxLineLength) {
|
|
||||||
// Add a last newline
|
|
||||||
outBuff[e + 4] = NEW_LINE;
|
|
||||||
e++;
|
|
||||||
}
|
|
||||||
e += 4;
|
|
||||||
}
|
|
||||||
|
|
||||||
assert (e == outBuff.length);
|
|
||||||
return outBuff;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* ******** D E C O D I N G M E T H O D S ******** */
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Decodes four bytes from array <var>source</var>
|
|
||||||
* and writes the resulting bytes (up to three of them)
|
|
||||||
* to <var>destination</var>.
|
|
||||||
* The source and destination arrays can be manipulated
|
|
||||||
* anywhere along their length by specifying
|
|
||||||
* <var>srcOffset</var> and <var>destOffset</var>.
|
|
||||||
* This method does not check to make sure your arrays
|
|
||||||
* are large enough to accommodate <var>srcOffset</var> + 4 for
|
|
||||||
* the <var>source</var> array or <var>destOffset</var> + 3 for
|
|
||||||
* the <var>destination</var> array.
|
|
||||||
* This method returns the actual number of bytes that
|
|
||||||
* were converted from the Base64 encoding.
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* @param source the array to convert
|
|
||||||
* @param srcOffset the index where conversion begins
|
|
||||||
* @param destination the array to hold the conversion
|
|
||||||
* @param destOffset the index where output will be put
|
|
||||||
* @param decodabet the decodabet for decoding Base64 content
|
|
||||||
* @return the number of decoded bytes converted
|
|
||||||
* @since 1.3
|
|
||||||
*/
|
|
||||||
private static int decode4to3(byte[] source, int srcOffset,
|
|
||||||
byte[] destination, int destOffset, byte[] decodabet) {
|
|
||||||
// Example: Dk==
|
|
||||||
if (source[srcOffset + 2] == EQUALS_SIGN) {
|
|
||||||
int outBuff =
|
|
||||||
((decodabet[source[srcOffset]] << 24) >>> 6)
|
|
||||||
| ((decodabet[source[srcOffset + 1]] << 24) >>> 12);
|
|
||||||
|
|
||||||
destination[destOffset] = (byte) (outBuff >>> 16);
|
|
||||||
return 1;
|
|
||||||
} else if (source[srcOffset + 3] == EQUALS_SIGN) {
|
|
||||||
// Example: DkL=
|
|
||||||
int outBuff =
|
|
||||||
((decodabet[source[srcOffset]] << 24) >>> 6)
|
|
||||||
| ((decodabet[source[srcOffset + 1]] << 24) >>> 12)
|
|
||||||
| ((decodabet[source[srcOffset + 2]] << 24) >>> 18);
|
|
||||||
|
|
||||||
destination[destOffset] = (byte) (outBuff >>> 16);
|
|
||||||
destination[destOffset + 1] = (byte) (outBuff >>> 8);
|
|
||||||
return 2;
|
|
||||||
} else {
|
|
||||||
// Example: DkLE
|
|
||||||
int outBuff =
|
|
||||||
((decodabet[source[srcOffset]] << 24) >>> 6)
|
|
||||||
| ((decodabet[source[srcOffset + 1]] << 24) >>> 12)
|
|
||||||
| ((decodabet[source[srcOffset + 2]] << 24) >>> 18)
|
|
||||||
| ((decodabet[source[srcOffset + 3]] << 24) >>> 24);
|
|
||||||
|
|
||||||
destination[destOffset] = (byte) (outBuff >> 16);
|
|
||||||
destination[destOffset + 1] = (byte) (outBuff >> 8);
|
|
||||||
destination[destOffset + 2] = (byte) (outBuff);
|
|
||||||
return 3;
|
|
||||||
}
|
|
||||||
} // end decodeToBytes
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Decodes data from Base64 notation.
|
|
||||||
*
|
|
||||||
* @param s the string to decode (decoded in default encoding)
|
|
||||||
* @return the decoded data
|
|
||||||
* @since 1.4
|
|
||||||
*/
|
|
||||||
public static byte[] decode(String s) throws Base64DecoderException {
|
|
||||||
byte[] bytes = s.getBytes();
|
|
||||||
return decode(bytes, 0, bytes.length);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Decodes data from web safe Base64 notation.
|
|
||||||
* Web safe encoding uses '-' instead of '+', '_' instead of '/'
|
|
||||||
*
|
|
||||||
* @param s the string to decode (decoded in default encoding)
|
|
||||||
* @return the decoded data
|
|
||||||
*/
|
|
||||||
public static byte[] decodeWebSafe(String s) throws Base64DecoderException {
|
|
||||||
byte[] bytes = s.getBytes();
|
|
||||||
return decodeWebSafe(bytes, 0, bytes.length);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Decodes Base64 content in byte array format and returns
|
|
||||||
* the decoded byte array.
|
|
||||||
*
|
|
||||||
* @param source The Base64 encoded data
|
|
||||||
* @return decoded data
|
|
||||||
* @since 1.3
|
|
||||||
* @throws Base64DecoderException
|
|
||||||
*/
|
|
||||||
public static byte[] decode(byte[] source) throws Base64DecoderException {
|
|
||||||
return decode(source, 0, source.length);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Decodes web safe Base64 content in byte array format and returns
|
|
||||||
* the decoded data.
|
|
||||||
* Web safe encoding uses '-' instead of '+', '_' instead of '/'
|
|
||||||
*
|
|
||||||
* @param source the string to decode (decoded in default encoding)
|
|
||||||
* @return the decoded data
|
|
||||||
*/
|
|
||||||
public static byte[] decodeWebSafe(byte[] source)
|
|
||||||
throws Base64DecoderException {
|
|
||||||
return decodeWebSafe(source, 0, source.length);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Decodes Base64 content in byte array format and returns
|
|
||||||
* the decoded byte array.
|
|
||||||
*
|
|
||||||
* @param source the Base64 encoded data
|
|
||||||
* @param off the offset of where to begin decoding
|
|
||||||
* @param len the length of characters to decode
|
|
||||||
* @return decoded data
|
|
||||||
* @since 1.3
|
|
||||||
* @throws Base64DecoderException
|
|
||||||
*/
|
|
||||||
public static byte[] decode(byte[] source, int off, int len)
|
|
||||||
throws Base64DecoderException {
|
|
||||||
return decode(source, off, len, DECODABET);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Decodes web safe Base64 content in byte array format and returns
|
|
||||||
* the decoded byte array.
|
|
||||||
* Web safe encoding uses '-' instead of '+', '_' instead of '/'
|
|
||||||
*
|
|
||||||
* @param source the Base64 encoded data
|
|
||||||
* @param off the offset of where to begin decoding
|
|
||||||
* @param len the length of characters to decode
|
|
||||||
* @return decoded data
|
|
||||||
*/
|
|
||||||
public static byte[] decodeWebSafe(byte[] source, int off, int len)
|
|
||||||
throws Base64DecoderException {
|
|
||||||
return decode(source, off, len, WEBSAFE_DECODABET);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Decodes Base64 content using the supplied decodabet and returns
|
|
||||||
* the decoded byte array.
|
|
||||||
*
|
|
||||||
* @param source the Base64 encoded data
|
|
||||||
* @param off the offset of where to begin decoding
|
|
||||||
* @param len the length of characters to decode
|
|
||||||
* @param decodabet the decodabet for decoding Base64 content
|
|
||||||
* @return decoded data
|
|
||||||
*/
|
|
||||||
public static byte[] decode(byte[] source, int off, int len, byte[] decodabet)
|
|
||||||
throws Base64DecoderException {
|
|
||||||
int len34 = len * 3 / 4;
|
|
||||||
byte[] outBuff = new byte[2 + len34]; // Upper limit on size of output
|
|
||||||
int outBuffPosn = 0;
|
|
||||||
|
|
||||||
byte[] b4 = new byte[4];
|
|
||||||
int b4Posn = 0;
|
|
||||||
int i = 0;
|
|
||||||
byte sbiCrop = 0;
|
|
||||||
byte sbiDecode = 0;
|
|
||||||
for (i = 0; i < len; i++) {
|
|
||||||
sbiCrop = (byte) (source[i + off] & 0x7f); // Only the low seven bits
|
|
||||||
sbiDecode = decodabet[sbiCrop];
|
|
||||||
|
|
||||||
if (sbiDecode >= WHITE_SPACE_ENC) { // White space Equals sign or better
|
|
||||||
if (sbiDecode >= EQUALS_SIGN_ENC) {
|
|
||||||
// An equals sign (for padding) must not occur at position 0 or 1
|
|
||||||
// and must be the last byte[s] in the encoded value
|
|
||||||
if (sbiCrop == EQUALS_SIGN) {
|
|
||||||
int bytesLeft = len - i;
|
|
||||||
byte lastByte = (byte) (source[len - 1 + off] & 0x7f);
|
|
||||||
if (b4Posn == 0 || b4Posn == 1) {
|
|
||||||
throw new Base64DecoderException(
|
|
||||||
"invalid padding byte '=' at byte offset " + i);
|
|
||||||
} else if ((b4Posn == 3 && bytesLeft > 2)
|
|
||||||
|| (b4Posn == 4 && bytesLeft > 1)) {
|
|
||||||
throw new Base64DecoderException(
|
|
||||||
"padding byte '=' falsely signals end of encoded value "
|
|
||||||
+ "at offset " + i);
|
|
||||||
} else if (lastByte != EQUALS_SIGN && lastByte != NEW_LINE) {
|
|
||||||
throw new Base64DecoderException(
|
|
||||||
"encoded value has invalid trailing byte");
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
b4[b4Posn++] = sbiCrop;
|
|
||||||
if (b4Posn == 4) {
|
|
||||||
outBuffPosn += decode4to3(b4, 0, outBuff, outBuffPosn, decodabet);
|
|
||||||
b4Posn = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
throw new Base64DecoderException("Bad Base64 input character at " + i
|
|
||||||
+ ": " + source[i + off] + "(decimal)");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Because web safe encoding allows non padding base64 encodes, we
|
|
||||||
// need to pad the rest of the b4 buffer with equal signs when
|
|
||||||
// b4Posn != 0. There can be at most 2 equal signs at the end of
|
|
||||||
// four characters, so the b4 buffer must have two or three
|
|
||||||
// characters. This also catches the case where the input is
|
|
||||||
// padded with EQUALS_SIGN
|
|
||||||
if (b4Posn != 0) {
|
|
||||||
if (b4Posn == 1) {
|
|
||||||
throw new Base64DecoderException("single trailing character at offset "
|
|
||||||
+ (len - 1));
|
|
||||||
}
|
|
||||||
b4[b4Posn++] = EQUALS_SIGN;
|
|
||||||
outBuffPosn += decode4to3(b4, 0, outBuff, outBuffPosn, decodabet);
|
|
||||||
}
|
|
||||||
|
|
||||||
byte[] out = new byte[outBuffPosn];
|
|
||||||
System.arraycopy(outBuff, 0, out, 0, outBuffPosn);
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
// Copyright 2002, Google, Inc.
|
|
||||||
//
|
|
||||||
// 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.util;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Exception thrown when encountering an invalid Base64 input character.
|
|
||||||
*
|
|
||||||
* @author nelson
|
|
||||||
*/
|
|
||||||
public class Base64DecoderException extends Exception {
|
|
||||||
public Base64DecoderException() {
|
|
||||||
super();
|
|
||||||
}
|
|
||||||
|
|
||||||
public Base64DecoderException(String s) {
|
|
||||||
super(s);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static final long serialVersionUID = 1L;
|
|
||||||
}
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
/* Copyright (c) 2012 Google Inc.
|
|
||||||
*
|
|
||||||
* 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.util;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Exception thrown when something went wrong with in-app billing.
|
|
||||||
* An IabException has an associated IabResult (an error).
|
|
||||||
* To get the IAB result that caused this exception to be thrown,
|
|
||||||
* call {@link #getResult()}.
|
|
||||||
*/
|
|
||||||
public class IabException extends Exception {
|
|
||||||
IabResult mResult;
|
|
||||||
|
|
||||||
public IabException(IabResult r) {
|
|
||||||
this(r, null);
|
|
||||||
}
|
|
||||||
public IabException(int response, String message) {
|
|
||||||
this(new IabResult(response, message));
|
|
||||||
}
|
|
||||||
public IabException(IabResult r, Exception cause) {
|
|
||||||
super(r.getMessage(), cause);
|
|
||||||
mResult = r;
|
|
||||||
}
|
|
||||||
public IabException(int response, String message, Exception cause) {
|
|
||||||
this(new IabResult(response, message), cause);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Returns the IAB result (error) that this exception signals. */
|
|
||||||
public IabResult getResult() { return mResult; }
|
|
||||||
}
|
|
||||||
@@ -1,979 +0,0 @@
|
|||||||
/* Copyright (c) 2012 Google Inc.
|
|
||||||
*
|
|
||||||
* 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.util;
|
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.app.PendingIntent;
|
|
||||||
import android.content.ComponentName;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.content.IntentSender.SendIntentException;
|
|
||||||
import android.content.ServiceConnection;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.os.Handler;
|
|
||||||
import android.os.IBinder;
|
|
||||||
import android.os.RemoteException;
|
|
||||||
import android.text.TextUtils;
|
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
import com.android.vending.billing.IInAppBillingService;
|
|
||||||
|
|
||||||
import org.json.JSONException;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Provides convenience methods for in-app billing. You can create one instance of this
|
|
||||||
* class for your application and use it to process in-app billing operations.
|
|
||||||
* It provides synchronous (blocking) and asynchronous (non-blocking) methods for
|
|
||||||
* many common in-app billing operations, as well as automatic signature
|
|
||||||
* verification.
|
|
||||||
* <p>
|
|
||||||
* After instantiating, you must perform setup in order to start using the object.
|
|
||||||
* To perform setup, call the {@link #startSetup} method and provide a listener;
|
|
||||||
* that listener will be notified when setup is complete, after which (and not before)
|
|
||||||
* you may call other methods.
|
|
||||||
* <p>
|
|
||||||
* After setup is complete, you will typically want to request an inventory of owned
|
|
||||||
* items and subscriptions. See {@link #queryInventory}, {@link #queryInventoryAsync}
|
|
||||||
* and related methods.
|
|
||||||
* <p>
|
|
||||||
* When you are done with this object, don't forget to call {@link #dispose}
|
|
||||||
* to ensure proper cleanup. This object holds a binding to the in-app billing
|
|
||||||
* service, which will leak unless you dispose of it correctly. If you created
|
|
||||||
* the object on an Activity's onCreate method, then the recommended
|
|
||||||
* place to dispose of it is the Activity's onDestroy method.
|
|
||||||
* <p>
|
|
||||||
* A note about threading: When using this object from a background thread, you may
|
|
||||||
* call the blocking versions of methods; when using from a UI thread, call
|
|
||||||
* only the asynchronous versions and handle the results via callbacks.
|
|
||||||
* Also, notice that you can only call one asynchronous operation at a time;
|
|
||||||
* attempting to start a second asynchronous operation while the first one
|
|
||||||
* has not yet completed will result in an exception being thrown.
|
|
||||||
*
|
|
||||||
* @author Bruno Oliveira (Google)
|
|
||||||
*/
|
|
||||||
public class IabHelper {
|
|
||||||
// Is debug logging enabled?
|
|
||||||
boolean mDebugLog = false;
|
|
||||||
String mDebugTag = "IabHelper";
|
|
||||||
|
|
||||||
// Is setup done?
|
|
||||||
boolean mSetupDone = false;
|
|
||||||
|
|
||||||
// Has this object been disposed of? (If so, we should ignore callbacks, etc)
|
|
||||||
boolean mDisposed = false;
|
|
||||||
|
|
||||||
// Are subscriptions supported?
|
|
||||||
boolean mSubscriptionsSupported = false;
|
|
||||||
|
|
||||||
// Is an asynchronous operation in progress?
|
|
||||||
// (only one at a time can be in progress)
|
|
||||||
boolean mAsyncInProgress = false;
|
|
||||||
|
|
||||||
// (for logging/debugging)
|
|
||||||
// if mAsyncInProgress == true, what asynchronous operation is in progress?
|
|
||||||
String mAsyncOperation = "";
|
|
||||||
|
|
||||||
// Context we were passed during initialization
|
|
||||||
Context mContext;
|
|
||||||
|
|
||||||
// Connection to the service
|
|
||||||
IInAppBillingService mService;
|
|
||||||
ServiceConnection mServiceConn;
|
|
||||||
|
|
||||||
// The request code used to launch purchase flow
|
|
||||||
int mRequestCode;
|
|
||||||
|
|
||||||
// The item type of the current purchase flow
|
|
||||||
String mPurchasingItemType;
|
|
||||||
|
|
||||||
// Public key for verifying signature, in base64 encoding
|
|
||||||
String mSignatureBase64 = null;
|
|
||||||
|
|
||||||
// Billing response codes
|
|
||||||
public static final int BILLING_RESPONSE_RESULT_OK = 0;
|
|
||||||
public static final int BILLING_RESPONSE_RESULT_USER_CANCELED = 1;
|
|
||||||
public static final int BILLING_RESPONSE_RESULT_BILLING_UNAVAILABLE = 3;
|
|
||||||
public static final int BILLING_RESPONSE_RESULT_ITEM_UNAVAILABLE = 4;
|
|
||||||
public static final int BILLING_RESPONSE_RESULT_DEVELOPER_ERROR = 5;
|
|
||||||
public static final int BILLING_RESPONSE_RESULT_ERROR = 6;
|
|
||||||
public static final int BILLING_RESPONSE_RESULT_ITEM_ALREADY_OWNED = 7;
|
|
||||||
public static final int BILLING_RESPONSE_RESULT_ITEM_NOT_OWNED = 8;
|
|
||||||
|
|
||||||
// IAB Helper error codes
|
|
||||||
public static final int IABHELPER_ERROR_BASE = -1000;
|
|
||||||
public static final int IABHELPER_REMOTE_EXCEPTION = -1001;
|
|
||||||
public static final int IABHELPER_BAD_RESPONSE = -1002;
|
|
||||||
public static final int IABHELPER_VERIFICATION_FAILED = -1003;
|
|
||||||
public static final int IABHELPER_SEND_INTENT_FAILED = -1004;
|
|
||||||
public static final int IABHELPER_USER_CANCELLED = -1005;
|
|
||||||
public static final int IABHELPER_UNKNOWN_PURCHASE_RESPONSE = -1006;
|
|
||||||
public static final int IABHELPER_MISSING_TOKEN = -1007;
|
|
||||||
public static final int IABHELPER_UNKNOWN_ERROR = -1008;
|
|
||||||
public static final int IABHELPER_SUBSCRIPTIONS_NOT_AVAILABLE = -1009;
|
|
||||||
public static final int IABHELPER_INVALID_CONSUMPTION = -1010;
|
|
||||||
|
|
||||||
// Keys for the responses from InAppBillingService
|
|
||||||
public static final String RESPONSE_CODE = "RESPONSE_CODE";
|
|
||||||
public static final String RESPONSE_GET_SKU_DETAILS_LIST = "DETAILS_LIST";
|
|
||||||
public static final String RESPONSE_BUY_INTENT = "BUY_INTENT";
|
|
||||||
public static final String RESPONSE_INAPP_PURCHASE_DATA = "INAPP_PURCHASE_DATA";
|
|
||||||
public static final String RESPONSE_INAPP_SIGNATURE = "INAPP_DATA_SIGNATURE";
|
|
||||||
public static final String RESPONSE_INAPP_ITEM_LIST = "INAPP_PURCHASE_ITEM_LIST";
|
|
||||||
public static final String RESPONSE_INAPP_PURCHASE_DATA_LIST = "INAPP_PURCHASE_DATA_LIST";
|
|
||||||
public static final String RESPONSE_INAPP_SIGNATURE_LIST = "INAPP_DATA_SIGNATURE_LIST";
|
|
||||||
public static final String INAPP_CONTINUATION_TOKEN = "INAPP_CONTINUATION_TOKEN";
|
|
||||||
|
|
||||||
// Item types
|
|
||||||
public static final String ITEM_TYPE_INAPP = "inapp";
|
|
||||||
public static final String ITEM_TYPE_SUBS = "subs";
|
|
||||||
|
|
||||||
// some fields on the getSkuDetails response bundle
|
|
||||||
public static final String GET_SKU_DETAILS_ITEM_LIST = "ITEM_ID_LIST";
|
|
||||||
public static final String GET_SKU_DETAILS_ITEM_TYPE_LIST = "ITEM_TYPE_LIST";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates an instance. After creation, it will not yet be ready to use. You must perform
|
|
||||||
* setup by calling {@link #startSetup} and wait for setup to complete. This constructor does not
|
|
||||||
* block and is safe to call from a UI thread.
|
|
||||||
*
|
|
||||||
* @param ctx Your application or Activity context. Needed to bind to the in-app billing service.
|
|
||||||
* @param base64PublicKey Your application's public key, encoded in base64.
|
|
||||||
* This is used for verification of purchase signatures. You can find your app's base64-encoded
|
|
||||||
* public key in your application's page on Google Play Developer Console. Note that this
|
|
||||||
* is NOT your "developer public key".
|
|
||||||
*/
|
|
||||||
public IabHelper(Context ctx, String base64PublicKey) {
|
|
||||||
mContext = ctx.getApplicationContext();
|
|
||||||
mSignatureBase64 = base64PublicKey;
|
|
||||||
logDebug("IAB helper created.");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Enables or disable debug logging through LogCat.
|
|
||||||
*/
|
|
||||||
public void enableDebugLogging(boolean enable, String tag) {
|
|
||||||
checkNotDisposed();
|
|
||||||
mDebugLog = enable;
|
|
||||||
mDebugTag = tag;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void enableDebugLogging(boolean enable) {
|
|
||||||
checkNotDisposed();
|
|
||||||
mDebugLog = enable;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Callback for setup process. This listener's {@link #onIabSetupFinished} method is called
|
|
||||||
* when the setup process is complete.
|
|
||||||
*/
|
|
||||||
public interface OnIabSetupFinishedListener {
|
|
||||||
/**
|
|
||||||
* Called to notify that setup is complete.
|
|
||||||
*
|
|
||||||
* @param result The result of the setup process.
|
|
||||||
*/
|
|
||||||
void onIabSetupFinished(IabResult result);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Starts the setup process. This will start up the setup process asynchronously.
|
|
||||||
* You will be notified through the listener when the setup process is complete.
|
|
||||||
* This method is safe to call from a UI thread.
|
|
||||||
*
|
|
||||||
* @param listener The listener to notify when the setup process is complete.
|
|
||||||
*/
|
|
||||||
public void startSetup(final OnIabSetupFinishedListener listener) {
|
|
||||||
// If already set up, can't do it again.
|
|
||||||
checkNotDisposed();
|
|
||||||
if (mSetupDone) throw new IllegalStateException("IAB helper is already set up.");
|
|
||||||
|
|
||||||
// Connection to IAB service
|
|
||||||
logDebug("Starting in-app billing setup.");
|
|
||||||
mServiceConn = new ServiceConnection() {
|
|
||||||
@Override
|
|
||||||
public void onServiceDisconnected(ComponentName name) {
|
|
||||||
logDebug("Billing service disconnected.");
|
|
||||||
mService = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onServiceConnected(ComponentName name, IBinder service) {
|
|
||||||
if (mDisposed) return;
|
|
||||||
logDebug("Billing service connected.");
|
|
||||||
mService = IInAppBillingService.Stub.asInterface(service);
|
|
||||||
String packageName = mContext.getPackageName();
|
|
||||||
try {
|
|
||||||
logDebug("Checking for in-app billing 3 support.");
|
|
||||||
|
|
||||||
// check for in-app billing v3 support
|
|
||||||
int response = mService.isBillingSupported(3, packageName, ITEM_TYPE_INAPP);
|
|
||||||
if (response != BILLING_RESPONSE_RESULT_OK) {
|
|
||||||
if (listener != null) listener.onIabSetupFinished(new IabResult(response,
|
|
||||||
"Error checking for billing v3 support."));
|
|
||||||
|
|
||||||
// if in-app purchases aren't supported, neither are subscriptions.
|
|
||||||
mSubscriptionsSupported = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
logDebug("In-app billing version 3 supported for " + packageName);
|
|
||||||
|
|
||||||
// check for v3 subscriptions support
|
|
||||||
response = mService.isBillingSupported(3, packageName, ITEM_TYPE_SUBS);
|
|
||||||
if (response == BILLING_RESPONSE_RESULT_OK) {
|
|
||||||
logDebug("Subscriptions AVAILABLE.");
|
|
||||||
mSubscriptionsSupported = true;
|
|
||||||
} else {
|
|
||||||
logDebug("Subscriptions NOT AVAILABLE. Response: " + response);
|
|
||||||
}
|
|
||||||
|
|
||||||
mSetupDone = true;
|
|
||||||
} catch (RemoteException e) {
|
|
||||||
if (listener != null) {
|
|
||||||
listener.onIabSetupFinished(new IabResult(IABHELPER_REMOTE_EXCEPTION,
|
|
||||||
"RemoteException while setting up in-app billing."));
|
|
||||||
}
|
|
||||||
e.printStackTrace();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (listener != null) {
|
|
||||||
listener.onIabSetupFinished(new IabResult(BILLING_RESPONSE_RESULT_OK, "Setup successful."));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Intent serviceIntent = new Intent("ir.cafebazaar.pardakht.InAppBillingService.BIND");
|
|
||||||
// serviceIntent.setPackage("com.farsitel.bazaar");
|
|
||||||
Intent serviceIntent = new Intent("com.android.vending.billing.InAppBillingService.BIND");
|
|
||||||
serviceIntent.setPackage("com.android.vending");
|
|
||||||
if (!mContext.getPackageManager().queryIntentServices(serviceIntent, 0).isEmpty()) {
|
|
||||||
// service available to handle that Intent
|
|
||||||
mContext.bindService(serviceIntent, mServiceConn, Context.BIND_AUTO_CREATE);
|
|
||||||
} else {
|
|
||||||
// no service available to handle that Intent
|
|
||||||
if (listener != null) {
|
|
||||||
listener.onIabSetupFinished(
|
|
||||||
new IabResult(BILLING_RESPONSE_RESULT_BILLING_UNAVAILABLE,
|
|
||||||
"Billing service unavailable on device."));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Dispose of object, releasing resources. It's very important to call this
|
|
||||||
* method when you are done with this object. It will release any resources
|
|
||||||
* used by it such as service connections. Naturally, once the object is
|
|
||||||
* disposed of, it can't be used again.
|
|
||||||
*/
|
|
||||||
public void dispose() {
|
|
||||||
logDebug("Disposing.");
|
|
||||||
mSetupDone = false;
|
|
||||||
if (mServiceConn != null) {
|
|
||||||
logDebug("Unbinding from service.");
|
|
||||||
if (mContext != null) mContext.unbindService(mServiceConn);
|
|
||||||
}
|
|
||||||
mDisposed = true;
|
|
||||||
mContext = null;
|
|
||||||
mServiceConn = null;
|
|
||||||
mService = null;
|
|
||||||
mPurchaseListener = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void checkNotDisposed() {
|
|
||||||
if (mDisposed)
|
|
||||||
throw new IllegalStateException("IabHelper was disposed of, so it cannot be used.");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns whether subscriptions are supported.
|
|
||||||
*/
|
|
||||||
public boolean subscriptionsSupported() {
|
|
||||||
checkNotDisposed();
|
|
||||||
return mSubscriptionsSupported;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Callback that notifies when a purchase is finished.
|
|
||||||
*/
|
|
||||||
public interface OnIabPurchaseFinishedListener {
|
|
||||||
/**
|
|
||||||
* Called to notify that an in-app purchase finished. If the purchase was successful,
|
|
||||||
* then the sku parameter specifies which item was purchased. If the purchase failed,
|
|
||||||
* the sku and extraData parameters may or may not be null, depending on how far the purchase
|
|
||||||
* process went.
|
|
||||||
*
|
|
||||||
* @param result The result of the purchase.
|
|
||||||
* @param info The purchase information (null if purchase failed)
|
|
||||||
*/
|
|
||||||
void onIabPurchaseFinished(IabResult result, Purchase info);
|
|
||||||
}
|
|
||||||
|
|
||||||
// The listener registered on launchPurchaseFlow, which we have to call back when
|
|
||||||
// the purchase finishes
|
|
||||||
OnIabPurchaseFinishedListener mPurchaseListener;
|
|
||||||
|
|
||||||
public void launchPurchaseFlow(Activity act, String sku, int requestCode, OnIabPurchaseFinishedListener listener) {
|
|
||||||
launchPurchaseFlow(act, sku, requestCode, listener, "");
|
|
||||||
}
|
|
||||||
|
|
||||||
public void launchPurchaseFlow(Activity act, String sku, int requestCode,
|
|
||||||
OnIabPurchaseFinishedListener listener, String extraData) {
|
|
||||||
launchPurchaseFlow(act, sku, ITEM_TYPE_INAPP, requestCode, listener, extraData);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void launchSubscriptionPurchaseFlow(Activity act, String sku, int requestCode,
|
|
||||||
OnIabPurchaseFinishedListener listener) {
|
|
||||||
launchSubscriptionPurchaseFlow(act, sku, requestCode, listener, "");
|
|
||||||
}
|
|
||||||
|
|
||||||
public void launchSubscriptionPurchaseFlow(Activity act, String sku, int requestCode,
|
|
||||||
OnIabPurchaseFinishedListener listener, String extraData) {
|
|
||||||
launchPurchaseFlow(act, sku, ITEM_TYPE_SUBS, requestCode, listener, extraData);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initiate the UI flow for an in-app purchase. Call this method to initiate an in-app purchase,
|
|
||||||
* which will involve bringing up the Google Play screen. The calling activity will be paused while
|
|
||||||
* the user interacts with Google Play, and the result will be delivered via the activity's
|
|
||||||
* {@link android.app.Activity#onActivityResult} method, at which point you must call
|
|
||||||
* this object's {@link #handleActivityResult} method to continue the purchase flow. This method
|
|
||||||
* MUST be called from the UI thread of the Activity.
|
|
||||||
*
|
|
||||||
* @param act The calling activity.
|
|
||||||
* @param sku The sku of the item to purchase.
|
|
||||||
* @param itemType indicates if it's a product or a subscription (ITEM_TYPE_INAPP or ITEM_TYPE_SUBS)
|
|
||||||
* @param requestCode A request code (to differentiate from other responses --
|
|
||||||
* as in {@link android.app.Activity#startActivityForResult}).
|
|
||||||
* @param listener The listener to notify when the purchase process finishes
|
|
||||||
* @param extraData Extra data (developer payload), which will be returned with the purchase data
|
|
||||||
* when the purchase completes. This extra data will be permanently bound to that purchase
|
|
||||||
* and will always be returned when the purchase is queried.
|
|
||||||
*/
|
|
||||||
public void launchPurchaseFlow(Activity act, String sku, String itemType, int requestCode,
|
|
||||||
OnIabPurchaseFinishedListener listener, String extraData) {
|
|
||||||
checkNotDisposed();
|
|
||||||
checkSetupDone("launchPurchaseFlow");
|
|
||||||
flagStartAsync("launchPurchaseFlow");
|
|
||||||
IabResult result;
|
|
||||||
|
|
||||||
if (itemType.equals(ITEM_TYPE_SUBS) && !mSubscriptionsSupported) {
|
|
||||||
IabResult r = new IabResult(IABHELPER_SUBSCRIPTIONS_NOT_AVAILABLE,
|
|
||||||
"Subscriptions are not available.");
|
|
||||||
flagEndAsync();
|
|
||||||
if (listener != null) listener.onIabPurchaseFinished(r, null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
logDebug("Constructing buy intent for " + sku + ", item type: " + itemType);
|
|
||||||
Bundle buyIntentBundle = mService.getBuyIntent(3, mContext.getPackageName(), sku, itemType, extraData);
|
|
||||||
int response = getResponseCodeFromBundle(buyIntentBundle);
|
|
||||||
if (response != BILLING_RESPONSE_RESULT_OK) {
|
|
||||||
logError("Unable to buy item, Error response: " + getResponseDesc(response));
|
|
||||||
flagEndAsync();
|
|
||||||
result = new IabResult(response, "Unable to buy item");
|
|
||||||
if (listener != null) listener.onIabPurchaseFinished(result, null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
PendingIntent pendingIntent = buyIntentBundle.getParcelable(RESPONSE_BUY_INTENT);
|
|
||||||
logDebug("Launching buy intent for " + sku + ". Request code: " + requestCode);
|
|
||||||
mRequestCode = requestCode;
|
|
||||||
mPurchaseListener = listener;
|
|
||||||
mPurchasingItemType = itemType;
|
|
||||||
act.startIntentSenderForResult(pendingIntent.getIntentSender(),
|
|
||||||
requestCode, new Intent(),
|
|
||||||
Integer.valueOf(0), Integer.valueOf(0),
|
|
||||||
Integer.valueOf(0));
|
|
||||||
} catch (SendIntentException e) {
|
|
||||||
logError("SendIntentException while launching purchase flow for sku " + sku);
|
|
||||||
e.printStackTrace();
|
|
||||||
flagEndAsync();
|
|
||||||
|
|
||||||
result = new IabResult(IABHELPER_SEND_INTENT_FAILED, "Failed to send intent.");
|
|
||||||
if (listener != null) listener.onIabPurchaseFinished(result, null);
|
|
||||||
} catch (RemoteException e) {
|
|
||||||
logError("RemoteException while launching purchase flow for sku " + sku);
|
|
||||||
e.printStackTrace();
|
|
||||||
flagEndAsync();
|
|
||||||
|
|
||||||
result = new IabResult(IABHELPER_REMOTE_EXCEPTION, "Remote exception while starting purchase flow");
|
|
||||||
if (listener != null) listener.onIabPurchaseFinished(result, null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles an activity result that's part of the purchase flow in in-app billing. If you
|
|
||||||
* are calling {@link #launchPurchaseFlow}, then you must call this method from your
|
|
||||||
* Activity's {@link android.app.Activity@onActivityResult} method. This method
|
|
||||||
* MUST be called from the UI thread of the Activity.
|
|
||||||
*
|
|
||||||
* @param requestCode The requestCode as you received it.
|
|
||||||
* @param resultCode The resultCode as you received it.
|
|
||||||
* @param data The data (Intent) as you received it.
|
|
||||||
* @return Returns true if the result was related to a purchase flow and was handled;
|
|
||||||
* false if the result was not related to a purchase, in which case you should
|
|
||||||
* handle it normally.
|
|
||||||
*/
|
|
||||||
public boolean handleActivityResult(int requestCode, int resultCode, Intent data) {
|
|
||||||
IabResult result;
|
|
||||||
if (requestCode != mRequestCode) return false;
|
|
||||||
|
|
||||||
checkNotDisposed();
|
|
||||||
checkSetupDone("handleActivityResult");
|
|
||||||
|
|
||||||
// end of async purchase operation that started on launchPurchaseFlow
|
|
||||||
flagEndAsync();
|
|
||||||
|
|
||||||
if (data == null) {
|
|
||||||
logError("Null data in IAB activity result.");
|
|
||||||
result = new IabResult(IABHELPER_BAD_RESPONSE, "Null data in IAB result");
|
|
||||||
if (mPurchaseListener != null) mPurchaseListener.onIabPurchaseFinished(result, null);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
int responseCode = getResponseCodeFromIntent(data);
|
|
||||||
String purchaseData = data.getStringExtra(RESPONSE_INAPP_PURCHASE_DATA);
|
|
||||||
String dataSignature = data.getStringExtra(RESPONSE_INAPP_SIGNATURE);
|
|
||||||
|
|
||||||
if (resultCode == Activity.RESULT_OK && responseCode == BILLING_RESPONSE_RESULT_OK) {
|
|
||||||
logDebug("Successful resultcode from purchase activity.");
|
|
||||||
logDebug("Purchase data: " + purchaseData);
|
|
||||||
logDebug("Data signature: " + dataSignature);
|
|
||||||
logDebug("Extras: " + data.getExtras());
|
|
||||||
logDebug("Expected item type: " + mPurchasingItemType);
|
|
||||||
|
|
||||||
if (purchaseData == null || dataSignature == null) {
|
|
||||||
logError("BUG: either purchaseData or dataSignature is null.");
|
|
||||||
logDebug("Extras: " + data.getExtras().toString());
|
|
||||||
result = new IabResult(IABHELPER_UNKNOWN_ERROR, "IAB returned null purchaseData or dataSignature");
|
|
||||||
if (mPurchaseListener != null)
|
|
||||||
mPurchaseListener.onIabPurchaseFinished(result, null);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
Purchase purchase = null;
|
|
||||||
try {
|
|
||||||
purchase = new Purchase(mPurchasingItemType, purchaseData, dataSignature);
|
|
||||||
String sku = purchase.getSku();
|
|
||||||
|
|
||||||
// Verify signature
|
|
||||||
if (!Security.verifyPurchase(mSignatureBase64, purchaseData, dataSignature)) {
|
|
||||||
logError("Purchase signature verification FAILED for sku " + sku);
|
|
||||||
result = new IabResult(IABHELPER_VERIFICATION_FAILED, "Signature verification failed for sku " + sku);
|
|
||||||
if (mPurchaseListener != null)
|
|
||||||
mPurchaseListener.onIabPurchaseFinished(result, purchase);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
logDebug("Purchase signature successfully verified.");
|
|
||||||
} catch (JSONException e) {
|
|
||||||
logError("Failed to parse purchase data.");
|
|
||||||
e.printStackTrace();
|
|
||||||
result = new IabResult(IABHELPER_BAD_RESPONSE, "Failed to parse purchase data.");
|
|
||||||
if (mPurchaseListener != null)
|
|
||||||
mPurchaseListener.onIabPurchaseFinished(result, null);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mPurchaseListener != null) {
|
|
||||||
mPurchaseListener.onIabPurchaseFinished(new IabResult(BILLING_RESPONSE_RESULT_OK, "Success"), purchase);
|
|
||||||
}
|
|
||||||
} else if (resultCode == Activity.RESULT_OK) {
|
|
||||||
// result code was OK, but in-app billing response was not OK.
|
|
||||||
logDebug("Result code was OK but in-app billing response was not OK: " + getResponseDesc(responseCode));
|
|
||||||
if (mPurchaseListener != null) {
|
|
||||||
result = new IabResult(responseCode, "Problem purchashing item.");
|
|
||||||
mPurchaseListener.onIabPurchaseFinished(result, null);
|
|
||||||
}
|
|
||||||
} else if (resultCode == Activity.RESULT_CANCELED) {
|
|
||||||
logDebug("Purchase canceled - Response: " + getResponseDesc(responseCode));
|
|
||||||
result = new IabResult(IABHELPER_USER_CANCELLED, "User canceled.");
|
|
||||||
if (mPurchaseListener != null) mPurchaseListener.onIabPurchaseFinished(result, null);
|
|
||||||
} else {
|
|
||||||
logError("Purchase failed. Result code: " + Integer.toString(resultCode)
|
|
||||||
+ ". Response: " + getResponseDesc(responseCode));
|
|
||||||
result = new IabResult(IABHELPER_UNKNOWN_PURCHASE_RESPONSE, "Unknown purchase response.");
|
|
||||||
if (mPurchaseListener != null) mPurchaseListener.onIabPurchaseFinished(result, null);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Inventory queryInventory(boolean querySkuDetails, List<String> moreSkus) throws IabException {
|
|
||||||
return queryInventory(querySkuDetails, moreSkus, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Queries the inventory. This will query all owned items from the server, as well as
|
|
||||||
* information on additional skus, if specified. This method may block or take long to execute.
|
|
||||||
* Do not call from a UI thread. For that, use the non-blocking version {@link #refreshInventoryAsync}.
|
|
||||||
*
|
|
||||||
* @param querySkuDetails if true, SKU details (price, description, etc) will be queried as well
|
|
||||||
* as purchase information.
|
|
||||||
* @param moreItemSkus additional PRODUCT skus to query information on, regardless of ownership.
|
|
||||||
* Ignored if null or if querySkuDetails is false.
|
|
||||||
* @param moreSubsSkus additional SUBSCRIPTIONS skus to query information on, regardless of ownership.
|
|
||||||
* Ignored if null or if querySkuDetails is false.
|
|
||||||
* @throws IabException if a problem occurs while refreshing the inventory.
|
|
||||||
*/
|
|
||||||
public Inventory queryInventory(boolean querySkuDetails, List<String> moreItemSkus,
|
|
||||||
List<String> moreSubsSkus) throws IabException {
|
|
||||||
checkNotDisposed();
|
|
||||||
checkSetupDone("queryInventory");
|
|
||||||
try {
|
|
||||||
Inventory inv = new Inventory();
|
|
||||||
int r = queryPurchases(inv, ITEM_TYPE_INAPP);
|
|
||||||
if (r != BILLING_RESPONSE_RESULT_OK) {
|
|
||||||
throw new IabException(r, "Error refreshing inventory (querying owned items).");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (querySkuDetails) {
|
|
||||||
r = querySkuDetails(ITEM_TYPE_INAPP, inv, moreItemSkus);
|
|
||||||
if (r != BILLING_RESPONSE_RESULT_OK) {
|
|
||||||
throw new IabException(r, "Error refreshing inventory (querying prices of items).");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// if subscriptions are supported, then also query for subscriptions
|
|
||||||
if (mSubscriptionsSupported) {
|
|
||||||
r = queryPurchases(inv, ITEM_TYPE_SUBS);
|
|
||||||
if (r != BILLING_RESPONSE_RESULT_OK) {
|
|
||||||
throw new IabException(r, "Error refreshing inventory (querying owned subscriptions).");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (querySkuDetails) {
|
|
||||||
r = querySkuDetails(ITEM_TYPE_SUBS, inv, moreItemSkus);
|
|
||||||
if (r != BILLING_RESPONSE_RESULT_OK) {
|
|
||||||
throw new IabException(r, "Error refreshing inventory (querying prices of subscriptions).");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return inv;
|
|
||||||
} catch (RemoteException e) {
|
|
||||||
throw new IabException(IABHELPER_REMOTE_EXCEPTION, "Remote exception while refreshing inventory.", e);
|
|
||||||
} catch (JSONException e) {
|
|
||||||
throw new IabException(IABHELPER_BAD_RESPONSE, "Error parsing JSON response while refreshing inventory.", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Listener that notifies when an inventory query operation completes.
|
|
||||||
*/
|
|
||||||
public interface QueryInventoryFinishedListener {
|
|
||||||
/**
|
|
||||||
* Called to notify that an inventory query operation completed.
|
|
||||||
*
|
|
||||||
* @param result The result of the operation.
|
|
||||||
* @param inv The inventory.
|
|
||||||
*/
|
|
||||||
void onQueryInventoryFinished(IabResult result, Inventory inv);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Asynchronous wrapper for inventory query. This will perform an inventory
|
|
||||||
* query as described in {@link #queryInventory}, but will do so asynchronously
|
|
||||||
* and call back the specified listener upon completion. This method is safe to
|
|
||||||
* call from a UI thread.
|
|
||||||
*
|
|
||||||
* @param querySkuDetails as in {@link #queryInventory}
|
|
||||||
* @param moreSkus as in {@link #queryInventory}
|
|
||||||
* @param listener The listener to notify when the refresh operation completes.
|
|
||||||
*/
|
|
||||||
public void queryInventoryAsync(final boolean querySkuDetails,
|
|
||||||
final List<String> moreSkus,
|
|
||||||
final QueryInventoryFinishedListener listener) {
|
|
||||||
final Handler handler = new Handler();
|
|
||||||
checkNotDisposed();
|
|
||||||
checkSetupDone("queryInventory");
|
|
||||||
flagStartAsync("refresh inventory");
|
|
||||||
(new Thread(new Runnable() {
|
|
||||||
public void run() {
|
|
||||||
IabResult result = new IabResult(BILLING_RESPONSE_RESULT_OK, "Inventory refresh successful.");
|
|
||||||
Inventory inv = null;
|
|
||||||
try {
|
|
||||||
inv = queryInventory(querySkuDetails, moreSkus);
|
|
||||||
} catch (IabException ex) {
|
|
||||||
result = ex.getResult();
|
|
||||||
}
|
|
||||||
|
|
||||||
flagEndAsync();
|
|
||||||
|
|
||||||
final IabResult result_f = result;
|
|
||||||
final Inventory inv_f = inv;
|
|
||||||
if (!mDisposed && listener != null) {
|
|
||||||
handler.post(new Runnable() {
|
|
||||||
public void run() {
|
|
||||||
listener.onQueryInventoryFinished(result_f, inv_f);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})).start();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void queryInventoryAsync(QueryInventoryFinishedListener listener) {
|
|
||||||
queryInventoryAsync(true, null, listener);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void queryInventoryAsync(boolean querySkuDetails, QueryInventoryFinishedListener listener) {
|
|
||||||
queryInventoryAsync(querySkuDetails, null, listener);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Consumes a given in-app product. Consuming can only be done on an item
|
|
||||||
* that's owned, and as a result of consumption, the user will no longer own it.
|
|
||||||
* This method may block or take long to return. Do not call from the UI thread.
|
|
||||||
* For that, see {@link #consumeAsync}.
|
|
||||||
*
|
|
||||||
* @param itemInfo The PurchaseInfo that represents the item to consume.
|
|
||||||
* @throws IabException if there is a problem during consumption.
|
|
||||||
*/
|
|
||||||
void consume(Purchase itemInfo) throws IabException {
|
|
||||||
checkNotDisposed();
|
|
||||||
checkSetupDone("consume");
|
|
||||||
|
|
||||||
if (!itemInfo.mItemType.equals(ITEM_TYPE_INAPP)) {
|
|
||||||
throw new IabException(IABHELPER_INVALID_CONSUMPTION,
|
|
||||||
"Items of type '" + itemInfo.mItemType + "' can't be consumed.");
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
String token = itemInfo.getToken();
|
|
||||||
String sku = itemInfo.getSku();
|
|
||||||
if (token == null || token.equals("")) {
|
|
||||||
logError("Can't consume " + sku + ". No token.");
|
|
||||||
throw new IabException(IABHELPER_MISSING_TOKEN, "PurchaseInfo is missing token for sku: "
|
|
||||||
+ sku + " " + itemInfo);
|
|
||||||
}
|
|
||||||
|
|
||||||
logDebug("Consuming sku: " + sku + ", token: " + token);
|
|
||||||
int response = mService.consumePurchase(3, mContext.getPackageName(), token);
|
|
||||||
if (response == BILLING_RESPONSE_RESULT_OK) {
|
|
||||||
logDebug("Successfully consumed sku: " + sku);
|
|
||||||
} else {
|
|
||||||
logDebug("Error consuming consuming sku " + sku + ". " + getResponseDesc(response));
|
|
||||||
throw new IabException(response, "Error consuming sku " + sku);
|
|
||||||
}
|
|
||||||
} catch (RemoteException e) {
|
|
||||||
throw new IabException(IABHELPER_REMOTE_EXCEPTION, "Remote exception while consuming. PurchaseInfo: " + itemInfo, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Callback that notifies when a consumption operation finishes.
|
|
||||||
*/
|
|
||||||
public interface OnConsumeFinishedListener {
|
|
||||||
/**
|
|
||||||
* Called to notify that a consumption has finished.
|
|
||||||
*
|
|
||||||
* @param purchase The purchase that was (or was to be) consumed.
|
|
||||||
* @param result The result of the consumption operation.
|
|
||||||
*/
|
|
||||||
void onConsumeFinished(Purchase purchase, IabResult result);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Callback that notifies when a multi-item consumption operation finishes.
|
|
||||||
*/
|
|
||||||
public interface OnConsumeMultiFinishedListener {
|
|
||||||
/**
|
|
||||||
* Called to notify that a consumption of multiple items has finished.
|
|
||||||
*
|
|
||||||
* @param purchases The purchases that were (or were to be) consumed.
|
|
||||||
* @param results The results of each consumption operation, corresponding to each
|
|
||||||
* sku.
|
|
||||||
*/
|
|
||||||
void onConsumeMultiFinished(List<Purchase> purchases, List<IabResult> results);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Asynchronous wrapper to item consumption. Works like {@link #consume}, but
|
|
||||||
* performs the consumption in the background and notifies completion through
|
|
||||||
* the provided listener. This method is safe to call from a UI thread.
|
|
||||||
*
|
|
||||||
* @param purchase The purchase to be consumed.
|
|
||||||
* @param listener The listener to notify when the consumption operation finishes.
|
|
||||||
*/
|
|
||||||
public void consumeAsync(Purchase purchase, OnConsumeFinishedListener listener) {
|
|
||||||
checkNotDisposed();
|
|
||||||
checkSetupDone("consume");
|
|
||||||
List<Purchase> purchases = new ArrayList<Purchase>();
|
|
||||||
purchases.add(purchase);
|
|
||||||
consumeAsyncInternal(purchases, listener, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Same as {@link consumeAsync}, but for multiple items at once.
|
|
||||||
*
|
|
||||||
* @param purchases The list of PurchaseInfo objects representing the purchases to consume.
|
|
||||||
* @param listener The listener to notify when the consumption operation finishes.
|
|
||||||
*/
|
|
||||||
public void consumeAsync(List<Purchase> purchases, OnConsumeMultiFinishedListener listener) {
|
|
||||||
checkNotDisposed();
|
|
||||||
checkSetupDone("consume");
|
|
||||||
consumeAsyncInternal(purchases, null, listener);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a human-readable description for the given response code.
|
|
||||||
*
|
|
||||||
* @param code The response code
|
|
||||||
* @return A human-readable string explaining the result code.
|
|
||||||
* It also includes the result code numerically.
|
|
||||||
*/
|
|
||||||
public static String getResponseDesc(int code) {
|
|
||||||
String[] iab_msgs = ("0:OK/1:User Canceled/2:Unknown/" +
|
|
||||||
"3:Billing Unavailable/4:Item unavailable/" +
|
|
||||||
"5:Developer Error/6:Error/7:Item Already Owned/" +
|
|
||||||
"8:Item not owned").split("/");
|
|
||||||
String[] iabhelper_msgs = ("0:OK/-1001:Remote exception during initialization/" +
|
|
||||||
"-1002:Bad response received/" +
|
|
||||||
"-1003:Purchase signature verification failed/" +
|
|
||||||
"-1004:Send intent failed/" +
|
|
||||||
"-1005:User cancelled/" +
|
|
||||||
"-1006:Unknown purchase response/" +
|
|
||||||
"-1007:Missing token/" +
|
|
||||||
"-1008:Unknown error/" +
|
|
||||||
"-1009:Subscriptions not available/" +
|
|
||||||
"-1010:Invalid consumption attempt").split("/");
|
|
||||||
|
|
||||||
if (code <= IABHELPER_ERROR_BASE) {
|
|
||||||
int index = IABHELPER_ERROR_BASE - code;
|
|
||||||
if (index >= 0 && index < iabhelper_msgs.length) return iabhelper_msgs[index];
|
|
||||||
else return String.valueOf(code) + ":Unknown IAB Helper Error";
|
|
||||||
} else if (code < 0 || code >= iab_msgs.length)
|
|
||||||
return String.valueOf(code) + ":Unknown";
|
|
||||||
else
|
|
||||||
return iab_msgs[code];
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// Checks that setup was done; if not, throws an exception.
|
|
||||||
void checkSetupDone(String operation) {
|
|
||||||
if (!mSetupDone) {
|
|
||||||
logError("Illegal state for operation (" + operation + "): IAB helper is not set up.");
|
|
||||||
throw new IllegalStateException("IAB helper is not set up. Can't perform operation: " + operation);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Workaround to bug where sometimes response codes come as Long instead of Integer
|
|
||||||
int getResponseCodeFromBundle(Bundle b) {
|
|
||||||
Object o = b.get(RESPONSE_CODE);
|
|
||||||
if (o == null) {
|
|
||||||
logDebug("Bundle with null response code, assuming OK (known issue)");
|
|
||||||
return BILLING_RESPONSE_RESULT_OK;
|
|
||||||
} else if (o instanceof Integer) return ((Integer) o).intValue();
|
|
||||||
else if (o instanceof Long) return (int) ((Long) o).longValue();
|
|
||||||
else {
|
|
||||||
logError("Unexpected type for bundle response code.");
|
|
||||||
logError(o.getClass().getName());
|
|
||||||
throw new RuntimeException("Unexpected type for bundle response code: " + o.getClass().getName());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Workaround to bug where sometimes response codes come as Long instead of Integer
|
|
||||||
int getResponseCodeFromIntent(Intent i) {
|
|
||||||
Object o = i.getExtras().get(RESPONSE_CODE);
|
|
||||||
if (o == null) {
|
|
||||||
logError("Intent with no response code, assuming OK (known issue)");
|
|
||||||
return BILLING_RESPONSE_RESULT_OK;
|
|
||||||
} else if (o instanceof Integer) return ((Integer) o).intValue();
|
|
||||||
else if (o instanceof Long) return (int) ((Long) o).longValue();
|
|
||||||
else {
|
|
||||||
logError("Unexpected type for intent response code.");
|
|
||||||
logError(o.getClass().getName());
|
|
||||||
throw new RuntimeException("Unexpected type for intent response code: " + o.getClass().getName());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void flagStartAsync(String operation) {
|
|
||||||
if (mAsyncInProgress) throw new IllegalStateException("Can't start async operation (" +
|
|
||||||
operation + ") because another async operation(" + mAsyncOperation + ") is in progress.");
|
|
||||||
mAsyncOperation = operation;
|
|
||||||
mAsyncInProgress = true;
|
|
||||||
logDebug("Starting async operation: " + operation);
|
|
||||||
}
|
|
||||||
|
|
||||||
void flagEndAsync() {
|
|
||||||
logDebug("Ending async operation: " + mAsyncOperation);
|
|
||||||
mAsyncOperation = "";
|
|
||||||
mAsyncInProgress = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
int queryPurchases(Inventory inv, String itemType) throws JSONException, RemoteException {
|
|
||||||
// Query purchases
|
|
||||||
logDebug("Querying owned items, item type: " + itemType);
|
|
||||||
logDebug("Package name: " + mContext.getPackageName());
|
|
||||||
boolean verificationFailed = false;
|
|
||||||
String continueToken = null;
|
|
||||||
|
|
||||||
do {
|
|
||||||
logDebug("Calling getPurchases with continuation token: " + continueToken);
|
|
||||||
Bundle ownedItems = mService.getPurchases(3, mContext.getPackageName(),
|
|
||||||
itemType, continueToken);
|
|
||||||
|
|
||||||
int response = getResponseCodeFromBundle(ownedItems);
|
|
||||||
logDebug("Owned items response: " + String.valueOf(response));
|
|
||||||
if (response != BILLING_RESPONSE_RESULT_OK) {
|
|
||||||
logDebug("getPurchases() failed: " + getResponseDesc(response));
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
if (!ownedItems.containsKey(RESPONSE_INAPP_ITEM_LIST)
|
|
||||||
|| !ownedItems.containsKey(RESPONSE_INAPP_PURCHASE_DATA_LIST)
|
|
||||||
|| !ownedItems.containsKey(RESPONSE_INAPP_SIGNATURE_LIST)) {
|
|
||||||
logError("Bundle returned from getPurchases() doesn't contain required fields.");
|
|
||||||
return IABHELPER_BAD_RESPONSE;
|
|
||||||
}
|
|
||||||
|
|
||||||
ArrayList<String> ownedSkus = ownedItems.getStringArrayList(
|
|
||||||
RESPONSE_INAPP_ITEM_LIST);
|
|
||||||
ArrayList<String> purchaseDataList = ownedItems.getStringArrayList(
|
|
||||||
RESPONSE_INAPP_PURCHASE_DATA_LIST);
|
|
||||||
ArrayList<String> signatureList = ownedItems.getStringArrayList(
|
|
||||||
RESPONSE_INAPP_SIGNATURE_LIST);
|
|
||||||
|
|
||||||
for (int i = 0; i < purchaseDataList.size(); ++i) {
|
|
||||||
String purchaseData = purchaseDataList.get(i);
|
|
||||||
String signature = signatureList.get(i);
|
|
||||||
String sku = ownedSkus.get(i);
|
|
||||||
if (Security.verifyPurchase(mSignatureBase64, purchaseData, signature)) {
|
|
||||||
logDebug("Sku is owned: " + sku);
|
|
||||||
Purchase purchase = new Purchase(itemType, purchaseData, signature);
|
|
||||||
|
|
||||||
if (TextUtils.isEmpty(purchase.getToken())) {
|
|
||||||
logWarn("BUG: empty/null token!");
|
|
||||||
logDebug("Purchase data: " + purchaseData);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Record ownership and token
|
|
||||||
inv.addPurchase(purchase);
|
|
||||||
} else {
|
|
||||||
logWarn("Purchase signature verification **FAILED**. Not adding item.");
|
|
||||||
logDebug(" Purchase data: " + purchaseData);
|
|
||||||
logDebug(" Signature: " + signature);
|
|
||||||
verificationFailed = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
continueToken = ownedItems.getString(INAPP_CONTINUATION_TOKEN);
|
|
||||||
logDebug("Continuation token: " + continueToken);
|
|
||||||
} while (!TextUtils.isEmpty(continueToken));
|
|
||||||
|
|
||||||
return verificationFailed ? IABHELPER_VERIFICATION_FAILED : BILLING_RESPONSE_RESULT_OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
int querySkuDetails(String itemType, Inventory inv, List<String> moreSkus)
|
|
||||||
throws RemoteException, JSONException {
|
|
||||||
logDebug("Querying SKU details.");
|
|
||||||
ArrayList<String> skuList = new ArrayList<String>();
|
|
||||||
skuList.addAll(inv.getAllOwnedSkus(itemType));
|
|
||||||
if (moreSkus != null) {
|
|
||||||
for (String sku : moreSkus) {
|
|
||||||
if (!skuList.contains(sku)) {
|
|
||||||
skuList.add(sku);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (skuList.size() == 0) {
|
|
||||||
logDebug("queryPrices: nothing to do because there are no SKUs.");
|
|
||||||
return BILLING_RESPONSE_RESULT_OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
Bundle querySkus = new Bundle();
|
|
||||||
querySkus.putStringArrayList(GET_SKU_DETAILS_ITEM_LIST, skuList);
|
|
||||||
Bundle skuDetails = mService.getSkuDetails(3, mContext.getPackageName(),
|
|
||||||
itemType, querySkus);
|
|
||||||
|
|
||||||
if (!skuDetails.containsKey(RESPONSE_GET_SKU_DETAILS_LIST)) {
|
|
||||||
int response = getResponseCodeFromBundle(skuDetails);
|
|
||||||
if (response != BILLING_RESPONSE_RESULT_OK) {
|
|
||||||
logDebug("getSkuDetails() failed: " + getResponseDesc(response));
|
|
||||||
return response;
|
|
||||||
} else {
|
|
||||||
logError("getSkuDetails() returned a bundle with neither an error nor a detail list.");
|
|
||||||
return IABHELPER_BAD_RESPONSE;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ArrayList<String> responseList = skuDetails.getStringArrayList(
|
|
||||||
RESPONSE_GET_SKU_DETAILS_LIST);
|
|
||||||
|
|
||||||
for (String thisResponse : responseList) {
|
|
||||||
SkuDetails d = new SkuDetails(itemType, thisResponse);
|
|
||||||
logDebug("Got sku details: " + d);
|
|
||||||
inv.addSkuDetails(d);
|
|
||||||
}
|
|
||||||
return BILLING_RESPONSE_RESULT_OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
void consumeAsyncInternal(final List<Purchase> purchases,
|
|
||||||
final OnConsumeFinishedListener singleListener,
|
|
||||||
final OnConsumeMultiFinishedListener multiListener) {
|
|
||||||
final Handler handler = new Handler();
|
|
||||||
flagStartAsync("consume");
|
|
||||||
(new Thread(new Runnable() {
|
|
||||||
public void run() {
|
|
||||||
final List<IabResult> results = new ArrayList<IabResult>();
|
|
||||||
for (Purchase purchase : purchases) {
|
|
||||||
try {
|
|
||||||
consume(purchase);
|
|
||||||
results.add(new IabResult(BILLING_RESPONSE_RESULT_OK, "Successful consume of sku " + purchase.getSku()));
|
|
||||||
} catch (IabException ex) {
|
|
||||||
results.add(ex.getResult());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
flagEndAsync();
|
|
||||||
if (!mDisposed && singleListener != null) {
|
|
||||||
handler.post(new Runnable() {
|
|
||||||
public void run() {
|
|
||||||
singleListener.onConsumeFinished(purchases.get(0), results.get(0));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (!mDisposed && multiListener != null) {
|
|
||||||
handler.post(new Runnable() {
|
|
||||||
public void run() {
|
|
||||||
multiListener.onConsumeMultiFinished(purchases, results);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})).start();
|
|
||||||
}
|
|
||||||
|
|
||||||
void logDebug(String msg) {
|
|
||||||
if (mDebugLog) Log.d(mDebugTag, msg);
|
|
||||||
}
|
|
||||||
|
|
||||||
void logError(String msg) {
|
|
||||||
Log.e(mDebugTag, "In-app billing error: " + msg);
|
|
||||||
}
|
|
||||||
|
|
||||||
void logWarn(String msg) {
|
|
||||||
Log.w(mDebugTag, "In-app billing warning: " + msg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
/* Copyright (c) 2012 Google Inc.
|
|
||||||
*
|
|
||||||
* 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.util;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Represents the result of an in-app billing operation.
|
|
||||||
* A result is composed of a response code (an integer) and possibly a
|
|
||||||
* message (String). You can get those by calling
|
|
||||||
* {@link #getResponse} and {@link #getMessage()}, respectively. You
|
|
||||||
* can also inquire whether a result is a success or a failure by
|
|
||||||
* calling {@link #isSuccess()} and {@link #isFailure()}.
|
|
||||||
*/
|
|
||||||
public class IabResult {
|
|
||||||
int mResponse;
|
|
||||||
String mMessage;
|
|
||||||
|
|
||||||
public IabResult(int response, String message) {
|
|
||||||
mResponse = response;
|
|
||||||
if (message == null || message.trim().length() == 0) {
|
|
||||||
mMessage = IabHelper.getResponseDesc(response);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
mMessage = message + " (response: " + IabHelper.getResponseDesc(response) + ")";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
public int getResponse() { return mResponse; }
|
|
||||||
public String getMessage() { return mMessage; }
|
|
||||||
public boolean isSuccess() { return mResponse == IabHelper.BILLING_RESPONSE_RESULT_OK; }
|
|
||||||
public boolean isFailure() { return !isSuccess(); }
|
|
||||||
public String toString() { return "IabResult: " + getMessage(); }
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,91 +0,0 @@
|
|||||||
/* Copyright (c) 2012 Google Inc.
|
|
||||||
*
|
|
||||||
* 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.util;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Represents a block of information about in-app items.
|
|
||||||
* An Inventory is returned by such methods as {@link IabHelper#queryInventory}.
|
|
||||||
*/
|
|
||||||
public class Inventory {
|
|
||||||
Map<String,SkuDetails> mSkuMap = new HashMap<String,SkuDetails>();
|
|
||||||
Map<String,Purchase> mPurchaseMap = new HashMap<String,Purchase>();
|
|
||||||
|
|
||||||
Inventory() { }
|
|
||||||
|
|
||||||
/** Returns the listing details for an in-app product. */
|
|
||||||
public SkuDetails getSkuDetails(String sku) {
|
|
||||||
return mSkuMap.get(sku);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Returns purchase information for a given product, or null if there is no purchase. */
|
|
||||||
public Purchase getPurchase(String sku) {
|
|
||||||
return mPurchaseMap.get(sku);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Returns whether or not there exists a purchase of the given product. */
|
|
||||||
public boolean hasPurchase(String sku) {
|
|
||||||
return mPurchaseMap.containsKey(sku);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Return whether or not details about the given product are available. */
|
|
||||||
public boolean hasDetails(String sku) {
|
|
||||||
return mSkuMap.containsKey(sku);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Erase a purchase (locally) from the inventory, given its product ID. This just
|
|
||||||
* modifies the Inventory object locally and has no effect on the server! This is
|
|
||||||
* useful when you have an existing Inventory object which you know to be up to date,
|
|
||||||
* and you have just consumed an item successfully, which means that erasing its
|
|
||||||
* purchase data from the Inventory you already have is quicker than querying for
|
|
||||||
* a new Inventory.
|
|
||||||
*/
|
|
||||||
public void erasePurchase(String sku) {
|
|
||||||
if (mPurchaseMap.containsKey(sku)) mPurchaseMap.remove(sku);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Returns a list of all owned product IDs. */
|
|
||||||
List<String> getAllOwnedSkus() {
|
|
||||||
return new ArrayList<String>(mPurchaseMap.keySet());
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Returns a list of all owned product IDs of a given type */
|
|
||||||
List<String> getAllOwnedSkus(String itemType) {
|
|
||||||
List<String> result = new ArrayList<String>();
|
|
||||||
for (Purchase p : mPurchaseMap.values()) {
|
|
||||||
if (p.getItemType().equals(itemType)) result.add(p.getSku());
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Returns a list of all purchases. */
|
|
||||||
List<Purchase> getAllPurchases() {
|
|
||||||
return new ArrayList<Purchase>(mPurchaseMap.values());
|
|
||||||
}
|
|
||||||
|
|
||||||
void addSkuDetails(SkuDetails d) {
|
|
||||||
mSkuMap.put(d.getSku(), d);
|
|
||||||
}
|
|
||||||
|
|
||||||
void addPurchase(Purchase p) {
|
|
||||||
mPurchaseMap.put(p.getSku(), p);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,540 +0,0 @@
|
|||||||
package com.v2ray.ang.util;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.os.Environment;
|
|
||||||
import android.os.Handler;
|
|
||||||
import android.os.Message;
|
|
||||||
import android.text.TextUtils;
|
|
||||||
|
|
||||||
import java.io.BufferedReader;
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.FileNotFoundException;
|
|
||||||
import java.io.FileOutputStream;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStreamReader;
|
|
||||||
import java.text.SimpleDateFormat;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Date;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reference to http://blog.csdn.net/way_ping_li/article/details/8487866
|
|
||||||
* and improved some features...
|
|
||||||
*/
|
|
||||||
public class LogRecorder {
|
|
||||||
|
|
||||||
public static final int LOG_LEVEL_NO_SET = 0;
|
|
||||||
|
|
||||||
public static final int LOG_BUFFER_MAIN = 1;
|
|
||||||
public static final int LOG_BUFFER_SYSTEM = 1 << 1;
|
|
||||||
public static final int LOG_BUFFER_RADIO = 1 << 2;
|
|
||||||
public static final int LOG_BUFFER_EVENTS = 1 << 3;
|
|
||||||
public static final int LOG_BUFFER_KERNEL = 1 << 4; // not be supported by now
|
|
||||||
|
|
||||||
public static final int LOG_BUFFER_DEFAULT = LOG_BUFFER_MAIN | LOG_BUFFER_SYSTEM;
|
|
||||||
|
|
||||||
public static final int INVALID_PID = -1;
|
|
||||||
|
|
||||||
public String mFileSuffix;
|
|
||||||
public String mFolderPath;
|
|
||||||
public int mFileSizeLimitation;
|
|
||||||
public int mLevel;
|
|
||||||
public List<String> mFilterTags = new ArrayList<>();
|
|
||||||
public int mPID = INVALID_PID;
|
|
||||||
|
|
||||||
public boolean mUseLogcatFileOut = false;
|
|
||||||
|
|
||||||
private LogDumper mLogDumper = null;
|
|
||||||
|
|
||||||
public static final int EVENT_RESTART_LOG = 1001;
|
|
||||||
|
|
||||||
private RestartHandler mHandler;
|
|
||||||
|
|
||||||
private static class RestartHandler extends Handler {
|
|
||||||
final LogRecorder logRecorder;
|
|
||||||
public RestartHandler(LogRecorder logRecorder) {
|
|
||||||
this.logRecorder = logRecorder;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void handleMessage(Message msg) {
|
|
||||||
if (msg.what == EVENT_RESTART_LOG) {
|
|
||||||
logRecorder.stop();
|
|
||||||
logRecorder.start();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public LogRecorder() {
|
|
||||||
mHandler = new RestartHandler(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void start() {
|
|
||||||
// make sure the out folder exist
|
|
||||||
// TODO support multi-phase path
|
|
||||||
File file = new File(mFolderPath);
|
|
||||||
if (!file.exists()) {
|
|
||||||
file.mkdirs();
|
|
||||||
}
|
|
||||||
|
|
||||||
String cmdStr = collectLogcatCommand();
|
|
||||||
|
|
||||||
if (mLogDumper != null) {
|
|
||||||
mLogDumper.stopDumping();
|
|
||||||
mLogDumper = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
mLogDumper = new LogDumper(mFolderPath, mFileSuffix, mFileSizeLimitation, cmdStr, mHandler);
|
|
||||||
mLogDumper.start();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void stop() {
|
|
||||||
// TODO maybe should clean the log buffer first?
|
|
||||||
if (mLogDumper != null) {
|
|
||||||
mLogDumper.stopDumping();
|
|
||||||
mLogDumper = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private String collectLogcatCommand() {
|
|
||||||
StringBuilder stringBuilder = new StringBuilder();
|
|
||||||
final String SPACE = " ";
|
|
||||||
stringBuilder.append("logcat");
|
|
||||||
|
|
||||||
// TODO select ring buffer, -b
|
|
||||||
|
|
||||||
// TODO set out format
|
|
||||||
stringBuilder.append(SPACE);
|
|
||||||
stringBuilder.append("-v time");
|
|
||||||
|
|
||||||
// append tag filters
|
|
||||||
String levelStr = getLevelStr();
|
|
||||||
|
|
||||||
if (!mFilterTags.isEmpty()) {
|
|
||||||
stringBuilder.append(SPACE);
|
|
||||||
stringBuilder.append("-s");
|
|
||||||
for (int i = 0; i < mFilterTags.size(); i++) {
|
|
||||||
String tag = mFilterTags.get(i) + ":" + levelStr;
|
|
||||||
stringBuilder.append(SPACE);
|
|
||||||
stringBuilder.append(tag);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (!TextUtils.isEmpty(levelStr)) {
|
|
||||||
stringBuilder.append(SPACE);
|
|
||||||
stringBuilder.append("*:" + levelStr);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// logcat -f , but the rotated count default is 4?
|
|
||||||
// can`t be sure to use that feature
|
|
||||||
if (mPID != INVALID_PID) {
|
|
||||||
mUseLogcatFileOut = false;
|
|
||||||
String pidStr = adjustPIDStr();
|
|
||||||
if (!TextUtils.isEmpty(pidStr)) {
|
|
||||||
stringBuilder.append(SPACE);
|
|
||||||
stringBuilder.append("|");
|
|
||||||
stringBuilder.append(SPACE);
|
|
||||||
stringBuilder.append("grep (" + pidStr + ")");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return stringBuilder.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
private String getLevelStr() {
|
|
||||||
switch (mLevel) {
|
|
||||||
case 2:
|
|
||||||
return "V";
|
|
||||||
case 3:
|
|
||||||
return "D";
|
|
||||||
case 4:
|
|
||||||
return "I";
|
|
||||||
case 5:
|
|
||||||
return "W";
|
|
||||||
case 6:
|
|
||||||
return "E";
|
|
||||||
case 7:
|
|
||||||
return "F";
|
|
||||||
}
|
|
||||||
|
|
||||||
return "V";
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Android`s user app pid is bigger than 1000.
|
|
||||||
*
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
private String adjustPIDStr() {
|
|
||||||
if (mPID == INVALID_PID) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
String pidStr = String.valueOf(mPID);
|
|
||||||
int length = pidStr.length();
|
|
||||||
if (length < 4) {
|
|
||||||
pidStr = " 0" + pidStr;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (length == 4) {
|
|
||||||
pidStr = " " + pidStr;
|
|
||||||
}
|
|
||||||
|
|
||||||
return pidStr;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private class LogDumper extends Thread {
|
|
||||||
final String logPath;
|
|
||||||
final String logFileSuffix;
|
|
||||||
final int logFileLimitation;
|
|
||||||
final String logCmd;
|
|
||||||
|
|
||||||
final RestartHandler restartHandler;
|
|
||||||
|
|
||||||
private Process logcatProc;
|
|
||||||
private BufferedReader mReader = null;
|
|
||||||
private FileOutputStream out = null;
|
|
||||||
|
|
||||||
private boolean mRunning = true;
|
|
||||||
final private Object mRunningLock = new Object();
|
|
||||||
|
|
||||||
private long currentFileSize;
|
|
||||||
|
|
||||||
public LogDumper(String folderPath, String suffix,
|
|
||||||
int fileSizeLimitation, String command,
|
|
||||||
RestartHandler handler) {
|
|
||||||
logPath = folderPath;
|
|
||||||
logFileSuffix = suffix;
|
|
||||||
logFileLimitation = fileSizeLimitation;
|
|
||||||
logCmd = command;
|
|
||||||
restartHandler = handler;
|
|
||||||
|
|
||||||
String date = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss")
|
|
||||||
.format(new Date(System.currentTimeMillis()));
|
|
||||||
String fileName = (TextUtils.isEmpty(logFileSuffix)) ? date : (logFileSuffix + "-"+ date);
|
|
||||||
try {
|
|
||||||
out = new FileOutputStream(new File(logPath, fileName + ".log"));
|
|
||||||
} catch (FileNotFoundException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void stopDumping() {
|
|
||||||
synchronized (mRunningLock) {
|
|
||||||
mRunning = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
try {
|
|
||||||
logcatProc = Runtime.getRuntime().exec(logCmd);
|
|
||||||
mReader = new BufferedReader(new InputStreamReader(
|
|
||||||
logcatProc.getInputStream()), 1024);
|
|
||||||
String line = null;
|
|
||||||
while (mRunning && (line = mReader.readLine()) != null) {
|
|
||||||
if (!mRunning) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (line.length() == 0) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (out != null && !line.isEmpty()) {
|
|
||||||
byte[] data = (line + "\n").getBytes();
|
|
||||||
out.write(data);
|
|
||||||
if (logFileLimitation != 0) {
|
|
||||||
currentFileSize += data.length;
|
|
||||||
if (currentFileSize > logFileLimitation*1024) {
|
|
||||||
restartHandler.sendEmptyMessage(EVENT_RESTART_LOG);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (IOException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
} finally {
|
|
||||||
if (logcatProc != null) {
|
|
||||||
logcatProc.destroy();
|
|
||||||
logcatProc = null;
|
|
||||||
}
|
|
||||||
if (mReader != null) {
|
|
||||||
try {
|
|
||||||
mReader.close();
|
|
||||||
mReader = null;
|
|
||||||
} catch (IOException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (out != null) {
|
|
||||||
try {
|
|
||||||
out.close();
|
|
||||||
} catch (IOException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
out = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class Builder {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* context object
|
|
||||||
*/
|
|
||||||
private Context mContext;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* the folder name that we save log files to,
|
|
||||||
* just folder name, not the whole path,
|
|
||||||
* if set this, will save log files to /sdcard/$mLogFolderName folder,
|
|
||||||
* use /sdcard/$ApplicationName as default.
|
|
||||||
*/
|
|
||||||
private String mLogFolderName;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* the whole folder path that we save log files to,
|
|
||||||
* this setting`s priority is bigger than folder name.
|
|
||||||
*/
|
|
||||||
private String mLogFolderPath;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* the log file suffix,
|
|
||||||
* if this is sot, it will be appended to log file name automatically
|
|
||||||
*/
|
|
||||||
private String mLogFileNameSuffix = "";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* single log file size limitation,
|
|
||||||
* in k-bytes, ex. set to 16, is 16KB limitation.
|
|
||||||
*/
|
|
||||||
private int mLogFileSizeLimitation = 0;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* log level, see android.util.Log, 2 - 7,
|
|
||||||
* if not be set, will use verbose as default
|
|
||||||
*/
|
|
||||||
private int mLogLevel = LogRecorder.LOG_LEVEL_NO_SET;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* can set several filter tags
|
|
||||||
* logcat -s ActivityManager:V SystemUI:V
|
|
||||||
*/
|
|
||||||
private List<String> mLogFilterTags = new ArrayList<>();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* filter through pid, by setting this with your APP PID,
|
|
||||||
* the log recorder will just record the APP`s own log,
|
|
||||||
* use one call: android.os.Process.myPid().
|
|
||||||
*/
|
|
||||||
private int mPID = LogRecorder.INVALID_PID;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* which log buffer to catch...
|
|
||||||
* <p/>
|
|
||||||
* Request alternate ring buffer, 'main', 'system', 'radio'
|
|
||||||
* or 'events'. Multiple -b parameters are allowed and the
|
|
||||||
* results are interleaved.
|
|
||||||
* <p/>
|
|
||||||
* The default is -b main -b system.
|
|
||||||
*/
|
|
||||||
private int mLogBuffersSelected = LogRecorder.LOG_BUFFER_DEFAULT;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* log output format, don`t support config yet, use $time format as default.
|
|
||||||
* <p/>
|
|
||||||
* Log messages contain a number of metadata fields, in addition to the tag and priority.
|
|
||||||
* You can modify the output format for messages so that they display a specific metadata
|
|
||||||
* field. To do so, you use the -v option and specify one of the supported output formats
|
|
||||||
* listed below.
|
|
||||||
* <p/>
|
|
||||||
* brief — Display priority/tag and PID of the process issuing the message.
|
|
||||||
* process — Display PID only.
|
|
||||||
* tag — Display the priority/tag only.
|
|
||||||
* thread - Display the priority, tag, and the PID(process ID) and TID(thread ID)
|
|
||||||
* of the thread issuing the message.
|
|
||||||
* raw — Display the raw log message, with no other metadata fields.
|
|
||||||
* time — Display the date, invocation time, priority/tag, and PID of
|
|
||||||
* the process issuing the message.
|
|
||||||
* threadtime — Display the date, invocation time, priority, tag, and the PID(process ID)
|
|
||||||
* and TID(thread ID) of the thread issuing the message.
|
|
||||||
* long — Display all metadata fields and separate messages with blank lines.
|
|
||||||
*/
|
|
||||||
private int mLogOutFormat;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* set log out folder name
|
|
||||||
*
|
|
||||||
* @param logFolderName folder name
|
|
||||||
* @return The same Builder.
|
|
||||||
*/
|
|
||||||
public Builder setLogFolderName(String logFolderName) {
|
|
||||||
this.mLogFolderName = logFolderName;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* set log out folder path
|
|
||||||
*
|
|
||||||
* @param logFolderPath out folder absolute path
|
|
||||||
* @return the same Builder
|
|
||||||
*/
|
|
||||||
public Builder setLogFolderPath(String logFolderPath) {
|
|
||||||
this.mLogFolderPath = logFolderPath;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* set log file name suffix
|
|
||||||
*
|
|
||||||
* @param logFileNameSuffix auto appened suffix
|
|
||||||
* @return the same Builder
|
|
||||||
*/
|
|
||||||
public Builder setLogFileNameSuffix(String logFileNameSuffix) {
|
|
||||||
this.mLogFileNameSuffix = logFileNameSuffix;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* set the file size limitation
|
|
||||||
*
|
|
||||||
* @param fileSizeLimitation file size limitation in KB
|
|
||||||
* @return the same Builder
|
|
||||||
*/
|
|
||||||
public Builder setLogFileSizeLimitation(int fileSizeLimitation) {
|
|
||||||
this.mLogFileSizeLimitation = fileSizeLimitation;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* set the log level
|
|
||||||
*
|
|
||||||
* @param logLevel log level, 2-7
|
|
||||||
* @return the same Builder
|
|
||||||
*/
|
|
||||||
public Builder setLogLevel(int logLevel) {
|
|
||||||
this.mLogLevel = logLevel;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* add log filterspec tag name, can add multiple ones,
|
|
||||||
* they use the same log level set by setLogLevel()
|
|
||||||
*
|
|
||||||
* @param tag tag name
|
|
||||||
* @return the same Builder
|
|
||||||
*/
|
|
||||||
public Builder addLogFilterTag(String tag) {
|
|
||||||
mLogFilterTags.add(tag);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* which process`s log
|
|
||||||
*
|
|
||||||
* @param mPID process id
|
|
||||||
* @return the same Builder
|
|
||||||
*/
|
|
||||||
public Builder setPID(int mPID) {
|
|
||||||
this.mPID = mPID;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* -b radio, -b main, -b system, -b events
|
|
||||||
* -b main -b system as default
|
|
||||||
*
|
|
||||||
* @param logBuffersSelected one of
|
|
||||||
* LOG_BUFFER_MAIN = 1 << 0;
|
|
||||||
* LOG_BUFFER_SYSTEM = 1 << 1;
|
|
||||||
* LOG_BUFFER_RADIO = 1 << 2;
|
|
||||||
* LOG_BUFFER_EVENTS = 1 << 3;
|
|
||||||
* LOG_BUFFER_KERNEL = 1 << 4;
|
|
||||||
* @return the same Builder
|
|
||||||
*/
|
|
||||||
public Builder setLogBufferSelected(int logBuffersSelected) {
|
|
||||||
this.mLogBuffersSelected = logBuffersSelected;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* sets log out format, -v parameter
|
|
||||||
*
|
|
||||||
* @param logOutFormat out format, like -v time
|
|
||||||
* @return the same Builder
|
|
||||||
*/
|
|
||||||
public Builder setLogOutFormat(int logOutFormat) {
|
|
||||||
this.mLogOutFormat = mLogOutFormat;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Builder(Context context) {
|
|
||||||
mContext = context;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* call this only if mLogFolderName and mLogFolderPath not
|
|
||||||
* be set both.
|
|
||||||
*
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
private void applyAppNameAsOutfolderName() {
|
|
||||||
try {
|
|
||||||
String appName = mContext.getPackageName();
|
|
||||||
String versionName = mContext.getPackageManager().getPackageInfo(
|
|
||||||
appName, 0).versionName;
|
|
||||||
int versionCode = mContext.getPackageManager()
|
|
||||||
.getPackageInfo(appName, 0).versionCode;
|
|
||||||
mLogFolderName = appName + "-" + versionName + "-" + versionCode;
|
|
||||||
mLogFolderPath = applyOutfolderPath();
|
|
||||||
} catch (Exception e) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private String applyOutfolderPath() {
|
|
||||||
String outPath = "";
|
|
||||||
if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
|
|
||||||
outPath = Environment.getExternalStorageDirectory()
|
|
||||||
.getAbsolutePath() + File.separator + mLogFolderName;
|
|
||||||
}
|
|
||||||
|
|
||||||
return outPath;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Combine all of the options that have been set and return
|
|
||||||
* a new {@link LogRecorder} object.
|
|
||||||
*/
|
|
||||||
public LogRecorder build() {
|
|
||||||
LogRecorder logRecorder = new LogRecorder();
|
|
||||||
|
|
||||||
// no folder name & folder path be set
|
|
||||||
if (TextUtils.isEmpty(mLogFolderName)
|
|
||||||
&& TextUtils.isEmpty(mLogFolderPath)) {
|
|
||||||
applyAppNameAsOutfolderName();
|
|
||||||
}
|
|
||||||
|
|
||||||
// make sure out path be set
|
|
||||||
if (TextUtils.isEmpty(mLogFolderPath)) {
|
|
||||||
mLogFolderPath = applyOutfolderPath();
|
|
||||||
}
|
|
||||||
|
|
||||||
logRecorder.mFolderPath = mLogFolderPath;
|
|
||||||
logRecorder.mFileSuffix = mLogFileNameSuffix;
|
|
||||||
logRecorder.mFileSizeLimitation = mLogFileSizeLimitation;
|
|
||||||
logRecorder.mLevel = mLogLevel;
|
|
||||||
if (!mLogFilterTags.isEmpty()) {
|
|
||||||
for (int i = 0; i < mLogFilterTags.size(); i++) {
|
|
||||||
logRecorder.mFilterTags.add(mLogFilterTags.get(i));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
logRecorder.mPID = mPID;
|
|
||||||
|
|
||||||
return logRecorder;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
/* Copyright (c) 2012 Google Inc.
|
|
||||||
*
|
|
||||||
* 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.util;
|
|
||||||
|
|
||||||
import org.json.JSONException;
|
|
||||||
import org.json.JSONObject;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Represents an in-app billing purchase.
|
|
||||||
*/
|
|
||||||
public class Purchase {
|
|
||||||
String mItemType; // ITEM_TYPE_INAPP or ITEM_TYPE_SUBS
|
|
||||||
String mOrderId;
|
|
||||||
String mPackageName;
|
|
||||||
String mSku;
|
|
||||||
long mPurchaseTime;
|
|
||||||
int mPurchaseState;
|
|
||||||
String mDeveloperPayload;
|
|
||||||
String mToken;
|
|
||||||
String mOriginalJson;
|
|
||||||
String mSignature;
|
|
||||||
|
|
||||||
public Purchase(String itemType, String jsonPurchaseInfo, String signature) throws JSONException {
|
|
||||||
mItemType = itemType;
|
|
||||||
mOriginalJson = jsonPurchaseInfo;
|
|
||||||
JSONObject o = new JSONObject(mOriginalJson);
|
|
||||||
mOrderId = o.optString("orderId");
|
|
||||||
mPackageName = o.optString("packageName");
|
|
||||||
mSku = o.optString("productId");
|
|
||||||
mPurchaseTime = o.optLong("purchaseTime");
|
|
||||||
mPurchaseState = o.optInt("purchaseState");
|
|
||||||
mDeveloperPayload = o.optString("developerPayload");
|
|
||||||
mToken = o.optString("token", o.optString("purchaseToken"));
|
|
||||||
mSignature = signature;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getItemType() { return mItemType; }
|
|
||||||
public String getOrderId() { return mOrderId; }
|
|
||||||
public String getPackageName() { return mPackageName; }
|
|
||||||
public String getSku() { return mSku; }
|
|
||||||
public long getPurchaseTime() { return mPurchaseTime; }
|
|
||||||
public int getPurchaseState() { return mPurchaseState; }
|
|
||||||
public String getDeveloperPayload() { return mDeveloperPayload; }
|
|
||||||
public String getToken() { return mToken; }
|
|
||||||
public String getOriginalJson() { return mOriginalJson; }
|
|
||||||
public String getSignature() { return mSignature; }
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String toString() { return "PurchaseInfo(type:" + mItemType + "):" + mOriginalJson; }
|
|
||||||
}
|
|
||||||
@@ -1,116 +0,0 @@
|
|||||||
package com.v2ray.ang.util;
|
|
||||||
|
|
||||||
import android.graphics.Bitmap;
|
|
||||||
import android.graphics.BitmapFactory;
|
|
||||||
|
|
||||||
import com.google.zxing.BarcodeFormat;
|
|
||||||
import com.google.zxing.BinaryBitmap;
|
|
||||||
import com.google.zxing.DecodeHintType;
|
|
||||||
import com.google.zxing.MultiFormatReader;
|
|
||||||
import com.google.zxing.RGBLuminanceSource;
|
|
||||||
import com.google.zxing.Result;
|
|
||||||
import com.google.zxing.common.GlobalHistogramBinarizer;
|
|
||||||
import com.google.zxing.common.HybridBinarizer;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.EnumMap;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 描述:解析二维码图片
|
|
||||||
*/
|
|
||||||
public class QRCodeDecoder {
|
|
||||||
public static final Map<DecodeHintType, Object> HINTS = new EnumMap<>(DecodeHintType.class);
|
|
||||||
|
|
||||||
static {
|
|
||||||
List<BarcodeFormat> allFormats = new ArrayList<>();
|
|
||||||
allFormats.add(BarcodeFormat.AZTEC);
|
|
||||||
allFormats.add(BarcodeFormat.CODABAR);
|
|
||||||
allFormats.add(BarcodeFormat.CODE_39);
|
|
||||||
allFormats.add(BarcodeFormat.CODE_93);
|
|
||||||
allFormats.add(BarcodeFormat.CODE_128);
|
|
||||||
allFormats.add(BarcodeFormat.DATA_MATRIX);
|
|
||||||
allFormats.add(BarcodeFormat.EAN_8);
|
|
||||||
allFormats.add(BarcodeFormat.EAN_13);
|
|
||||||
allFormats.add(BarcodeFormat.ITF);
|
|
||||||
allFormats.add(BarcodeFormat.MAXICODE);
|
|
||||||
allFormats.add(BarcodeFormat.PDF_417);
|
|
||||||
allFormats.add(BarcodeFormat.QR_CODE);
|
|
||||||
allFormats.add(BarcodeFormat.RSS_14);
|
|
||||||
allFormats.add(BarcodeFormat.RSS_EXPANDED);
|
|
||||||
allFormats.add(BarcodeFormat.UPC_A);
|
|
||||||
allFormats.add(BarcodeFormat.UPC_E);
|
|
||||||
allFormats.add(BarcodeFormat.UPC_EAN_EXTENSION);
|
|
||||||
HINTS.put(DecodeHintType.TRY_HARDER, BarcodeFormat.QR_CODE);
|
|
||||||
HINTS.put(DecodeHintType.POSSIBLE_FORMATS, allFormats);
|
|
||||||
HINTS.put(DecodeHintType.CHARACTER_SET, "utf-8");
|
|
||||||
}
|
|
||||||
|
|
||||||
private QRCodeDecoder() {
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 同步解析本地图片二维码。该方法是耗时操作,请在子线程中调用。
|
|
||||||
*
|
|
||||||
* @param picturePath 要解析的二维码图片本地路径
|
|
||||||
* @return 返回二维码图片里的内容 或 null
|
|
||||||
*/
|
|
||||||
public static String syncDecodeQRCode(String picturePath) {
|
|
||||||
return syncDecodeQRCode(getDecodeAbleBitmap(picturePath));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 同步解析bitmap二维码。该方法是耗时操作,请在子线程中调用。
|
|
||||||
*
|
|
||||||
* @param bitmap 要解析的二维码图片
|
|
||||||
* @return 返回二维码图片里的内容 或 null
|
|
||||||
*/
|
|
||||||
public static String syncDecodeQRCode(Bitmap bitmap) {
|
|
||||||
Result result = null;
|
|
||||||
RGBLuminanceSource source = null;
|
|
||||||
try {
|
|
||||||
int width = bitmap.getWidth();
|
|
||||||
int height = bitmap.getHeight();
|
|
||||||
int[] pixels = new int[width * height];
|
|
||||||
bitmap.getPixels(pixels, 0, width, 0, 0, width, height);
|
|
||||||
source = new RGBLuminanceSource(width, height, pixels);
|
|
||||||
result = new MultiFormatReader().decode(new BinaryBitmap(new HybridBinarizer(source)), HINTS);
|
|
||||||
return result.getText();
|
|
||||||
} catch (Exception e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
if (source != null) {
|
|
||||||
try {
|
|
||||||
result = new MultiFormatReader().decode(new BinaryBitmap(new GlobalHistogramBinarizer(source)), HINTS);
|
|
||||||
return result.getText();
|
|
||||||
} catch (Throwable e2) {
|
|
||||||
e2.printStackTrace();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 将本地图片文件转换成可解码二维码的 Bitmap。为了避免图片太大,这里对图片进行了压缩。感谢 https://github.com/devilsen 提的 PR
|
|
||||||
*
|
|
||||||
* @param picturePath 本地图片文件路径
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
private static Bitmap getDecodeAbleBitmap(String picturePath) {
|
|
||||||
try {
|
|
||||||
BitmapFactory.Options options = new BitmapFactory.Options();
|
|
||||||
options.inJustDecodeBounds = true;
|
|
||||||
BitmapFactory.decodeFile(picturePath, options);
|
|
||||||
int sampleSize = options.outHeight / 400;
|
|
||||||
if (sampleSize <= 0) {
|
|
||||||
sampleSize = 1;
|
|
||||||
}
|
|
||||||
options.inSampleSize = sampleSize;
|
|
||||||
options.inJustDecodeBounds = false;
|
|
||||||
return BitmapFactory.decodeFile(picturePath, options);
|
|
||||||
} catch (Exception e) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,119 +0,0 @@
|
|||||||
/* Copyright (c) 2012 Google Inc.
|
|
||||||
*
|
|
||||||
* 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.util;
|
|
||||||
|
|
||||||
import android.text.TextUtils;
|
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
import java.security.InvalidKeyException;
|
|
||||||
import java.security.KeyFactory;
|
|
||||||
import java.security.NoSuchAlgorithmException;
|
|
||||||
import java.security.PublicKey;
|
|
||||||
import java.security.Signature;
|
|
||||||
import java.security.SignatureException;
|
|
||||||
import java.security.spec.InvalidKeySpecException;
|
|
||||||
import java.security.spec.X509EncodedKeySpec;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Security-related methods. For a secure implementation, all of this code
|
|
||||||
* should be implemented on a server that communicates with the
|
|
||||||
* application on the device. For the sake of simplicity and clarity of this
|
|
||||||
* example, this code is included here and is executed on the device. If you
|
|
||||||
* must verify the purchases on the phone, you should obfuscate this code to
|
|
||||||
* make it harder for an attacker to replace the code with stubs that treat all
|
|
||||||
* purchases as verified.
|
|
||||||
*/
|
|
||||||
public class Security {
|
|
||||||
private static final String TAG = "IABUtil/Security";
|
|
||||||
|
|
||||||
private static final String KEY_FACTORY_ALGORITHM = "RSA";
|
|
||||||
private static final String SIGNATURE_ALGORITHM = "SHA1withRSA";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Verifies that the data was signed with the given signature, and returns
|
|
||||||
* the verified purchase. The data is in JSON format and signed
|
|
||||||
* with a private key. The data also contains the {@link PurchaseState}
|
|
||||||
* and product ID of the purchase.
|
|
||||||
* @param base64PublicKey the base64-encoded public key to use for verifying.
|
|
||||||
* @param signedData the signed JSON string (signed, not encrypted)
|
|
||||||
* @param signature the signature for the data, signed with the private key
|
|
||||||
*/
|
|
||||||
public static boolean verifyPurchase(String base64PublicKey, String signedData, String signature) {
|
|
||||||
if (TextUtils.isEmpty(signedData) || TextUtils.isEmpty(base64PublicKey) ||
|
|
||||||
TextUtils.isEmpty(signature)) {
|
|
||||||
Log.e(TAG, "Purchase verification failed: missing data.");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
PublicKey key = Security.generatePublicKey(base64PublicKey);
|
|
||||||
return Security.verify(key, signedData, signature);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generates a PublicKey instance from a string containing the
|
|
||||||
* Base64-encoded public key.
|
|
||||||
*
|
|
||||||
* @param encodedPublicKey Base64-encoded public key
|
|
||||||
* @throws IllegalArgumentException if encodedPublicKey is invalid
|
|
||||||
*/
|
|
||||||
public static PublicKey generatePublicKey(String encodedPublicKey) {
|
|
||||||
try {
|
|
||||||
byte[] decodedKey = Base64.decode(encodedPublicKey);
|
|
||||||
KeyFactory keyFactory = KeyFactory.getInstance(KEY_FACTORY_ALGORITHM);
|
|
||||||
return keyFactory.generatePublic(new X509EncodedKeySpec(decodedKey));
|
|
||||||
} catch (NoSuchAlgorithmException e) {
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
} catch (InvalidKeySpecException e) {
|
|
||||||
Log.e(TAG, "Invalid key specification.");
|
|
||||||
throw new IllegalArgumentException(e);
|
|
||||||
} catch (Base64DecoderException e) {
|
|
||||||
Log.e(TAG, "Base64 decoding failed.");
|
|
||||||
throw new IllegalArgumentException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Verifies that the signature from the server matches the computed
|
|
||||||
* signature on the data. Returns true if the data is correctly signed.
|
|
||||||
*
|
|
||||||
* @param publicKey public key associated with the developer account
|
|
||||||
* @param signedData signed data from server
|
|
||||||
* @param signature server signature
|
|
||||||
* @return true if the data and signature match
|
|
||||||
*/
|
|
||||||
public static boolean verify(PublicKey publicKey, String signedData, String signature) {
|
|
||||||
Signature sig;
|
|
||||||
try {
|
|
||||||
sig = Signature.getInstance(SIGNATURE_ALGORITHM);
|
|
||||||
sig.initVerify(publicKey);
|
|
||||||
sig.update(signedData.getBytes());
|
|
||||||
if (!sig.verify(Base64.decode(signature))) {
|
|
||||||
Log.e(TAG, "Signature verification failed.");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
} catch (NoSuchAlgorithmException e) {
|
|
||||||
Log.e(TAG, "NoSuchAlgorithmException.");
|
|
||||||
} catch (InvalidKeyException e) {
|
|
||||||
Log.e(TAG, "Invalid key specification.");
|
|
||||||
} catch (SignatureException e) {
|
|
||||||
Log.e(TAG, "Signature exception.");
|
|
||||||
} catch (Base64DecoderException e) {
|
|
||||||
Log.e(TAG, "Base64 decoding failed.");
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
/* Copyright (c) 2012 Google Inc.
|
|
||||||
*
|
|
||||||
* 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.util;
|
|
||||||
|
|
||||||
import org.json.JSONException;
|
|
||||||
import org.json.JSONObject;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Represents an in-app product's listing details.
|
|
||||||
*/
|
|
||||||
public class SkuDetails {
|
|
||||||
String mItemType;
|
|
||||||
String mSku;
|
|
||||||
String mType;
|
|
||||||
String mPrice;
|
|
||||||
String mTitle;
|
|
||||||
String mDescription;
|
|
||||||
String mJson;
|
|
||||||
|
|
||||||
public SkuDetails(String jsonSkuDetails) throws JSONException {
|
|
||||||
this(IabHelper.ITEM_TYPE_INAPP, jsonSkuDetails);
|
|
||||||
}
|
|
||||||
|
|
||||||
public SkuDetails(String itemType, String jsonSkuDetails) throws JSONException {
|
|
||||||
mItemType = itemType;
|
|
||||||
mJson = jsonSkuDetails;
|
|
||||||
JSONObject o = new JSONObject(mJson);
|
|
||||||
mSku = o.optString("productId");
|
|
||||||
mType = o.optString("type");
|
|
||||||
mPrice = o.optString("price");
|
|
||||||
mTitle = o.optString("title");
|
|
||||||
mDescription = o.optString("description");
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getSku() { return mSku; }
|
|
||||||
public String getType() { return mType; }
|
|
||||||
public String getPrice() { return mPrice; }
|
|
||||||
public String getTitle() { return mTitle; }
|
|
||||||
public String getDescription() { return mDescription; }
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String toString() {
|
|
||||||
return "SkuDetails:" + mJson;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,32 +1,28 @@
|
|||||||
package com.v2ray.ang
|
package com.v2ray.ang
|
||||||
|
|
||||||
//import com.squareup.leakcanary.LeakCanary
|
import androidx.multidex.MultiDexApplication
|
||||||
import android.support.multidex.MultiDexApplication
|
import androidx.preference.PreferenceManager
|
||||||
import com.v2ray.ang.util.AngConfigManager
|
import com.tencent.mmkv.MMKV
|
||||||
import me.dozen.dpreference.DPreference
|
|
||||||
import org.jetbrains.anko.defaultSharedPreferences
|
|
||||||
|
|
||||||
class AngApplication : MultiDexApplication() {
|
class AngApplication : MultiDexApplication() {
|
||||||
companion object {
|
companion object {
|
||||||
const val PREF_LAST_VERSION = "pref_last_version"
|
const val PREF_LAST_VERSION = "pref_last_version"
|
||||||
}
|
}
|
||||||
|
|
||||||
var curIndex = -1 //Current proxy that is opened. (Used to implement restart feature)
|
|
||||||
var firstRun = false
|
var firstRun = false
|
||||||
private set
|
private set
|
||||||
|
|
||||||
val defaultDPreference by lazy { DPreference(this, packageName + "_preferences") }
|
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
|
|
||||||
// LeakCanary.install(this)
|
// LeakCanary.install(this)
|
||||||
|
|
||||||
|
val defaultSharedPreferences = PreferenceManager.getDefaultSharedPreferences(this)
|
||||||
firstRun = defaultSharedPreferences.getInt(PREF_LAST_VERSION, 0) != BuildConfig.VERSION_CODE
|
firstRun = defaultSharedPreferences.getInt(PREF_LAST_VERSION, 0) != BuildConfig.VERSION_CODE
|
||||||
if (firstRun)
|
if (firstRun)
|
||||||
defaultSharedPreferences.edit().putInt(PREF_LAST_VERSION, BuildConfig.VERSION_CODE).apply()
|
defaultSharedPreferences.edit().putInt(PREF_LAST_VERSION, BuildConfig.VERSION_CODE).apply()
|
||||||
|
|
||||||
//Logger.init().logLevel(if (BuildConfig.DEBUG) LogLevel.FULL else LogLevel.NONE)
|
//Logger.init().logLevel(if (BuildConfig.DEBUG) LogLevel.FULL else LogLevel.NONE)
|
||||||
AngConfigManager.inject(this)
|
MMKV.initialize(this)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,15 +6,43 @@ package com.v2ray.ang
|
|||||||
*/
|
*/
|
||||||
object AppConfig {
|
object AppConfig {
|
||||||
const val ANG_PACKAGE = "com.v2ray.ang"
|
const val ANG_PACKAGE = "com.v2ray.ang"
|
||||||
|
const val DIR_ASSETS = "assets"
|
||||||
|
|
||||||
|
// legacy
|
||||||
const val ANG_CONFIG = "ang_config"
|
const val ANG_CONFIG = "ang_config"
|
||||||
const val PREF_CURR_CONFIG = "pref_v2ray_config"
|
|
||||||
const val PREF_CURR_CONFIG_GUID = "pref_v2ray_config_guid"
|
|
||||||
const val PREF_CURR_CONFIG_NAME = "pref_v2ray_config_name"
|
|
||||||
const val PREF_CURR_CONFIG_DOMAIN = "pref_v2ray_config_domain"
|
|
||||||
const val PREF_INAPP_BUY_IS_PREMIUM = "pref_inapp_buy_is_premium"
|
const val PREF_INAPP_BUY_IS_PREMIUM = "pref_inapp_buy_is_premium"
|
||||||
const val VMESS_PROTOCOL: String = "vmess://"
|
const val PREF_ROUTING_CUSTOM = "pref_routing_custom"
|
||||||
const val SS_PROTOCOL: String = "ss://"
|
|
||||||
const val SOCKS_PROTOCOL: String = "socks://"
|
// 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_SERVICE = "com.v2ray.ang.action.service"
|
||||||
const val BROADCAST_ACTION_ACTIVITY = "com.v2ray.ang.action.activity"
|
const val BROADCAST_ACTION_ACTIVITY = "com.v2ray.ang.action.activity"
|
||||||
const val BROADCAST_ACTION_WIDGET_CLICK = "com.v2ray.ang.action.widget.click"
|
const val BROADCAST_ACTION_WIDGET_CLICK = "com.v2ray.ang.action.widget.click"
|
||||||
@@ -25,9 +53,6 @@ object AppConfig {
|
|||||||
const val TASKER_EXTRA_BUNDLE_GUID = "tasker_extra_bundle_guid"
|
const val TASKER_EXTRA_BUNDLE_GUID = "tasker_extra_bundle_guid"
|
||||||
const val TASKER_DEFAULT_GUID = "Default"
|
const val TASKER_DEFAULT_GUID = "Default"
|
||||||
|
|
||||||
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 TAG_AGENT = "proxy"
|
const val TAG_AGENT = "proxy"
|
||||||
const val TAG_DIRECT = "direct"
|
const val TAG_DIRECT = "direct"
|
||||||
const val TAG_BLOCKED = "block"
|
const val TAG_BLOCKED = "block"
|
||||||
@@ -35,11 +60,17 @@ object AppConfig {
|
|||||||
const val androidpackagenamelistUrl = "https://raw.githubusercontent.com/2dust/androidpackagenamelist/master/proxy.txt"
|
const val androidpackagenamelistUrl = "https://raw.githubusercontent.com/2dust/androidpackagenamelist/master/proxy.txt"
|
||||||
const val v2rayCustomRoutingListUrl = "https://raw.githubusercontent.com/2dust/v2rayCustomRoutingList/master/"
|
const val v2rayCustomRoutingListUrl = "https://raw.githubusercontent.com/2dust/v2rayCustomRoutingList/master/"
|
||||||
const val v2rayNGIssues = "https://github.com/2dust/v2rayNG/issues"
|
const val v2rayNGIssues = "https://github.com/2dust/v2rayNG/issues"
|
||||||
const val promotionUrl = "https://1.2345345.xyz/ads.html"
|
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_AGENT = "1.1.1.1"
|
||||||
const val DNS_DIRECT = "223.5.5.5"
|
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_REGISTER_CLIENT = 1
|
||||||
const val MSG_STATE_RUNNING = 11
|
const val MSG_STATE_RUNNING = 11
|
||||||
const val MSG_STATE_NOT_RUNNING = 12
|
const val MSG_STATE_NOT_RUNNING = 12
|
||||||
@@ -50,12 +81,9 @@ object AppConfig {
|
|||||||
const val MSG_STATE_STOP = 4
|
const val MSG_STATE_STOP = 4
|
||||||
const val MSG_STATE_STOP_SUCCESS = 41
|
const val MSG_STATE_STOP_SUCCESS = 41
|
||||||
const val MSG_STATE_RESTART = 5
|
const val MSG_STATE_RESTART = 5
|
||||||
|
const val MSG_MEASURE_DELAY = 6
|
||||||
object EConfigType {
|
const val MSG_MEASURE_DELAY_SUCCESS = 61
|
||||||
val Vmess = 1
|
const val MSG_MEASURE_CONFIG = 7
|
||||||
val Custom = 2
|
const val MSG_MEASURE_CONFIG_SUCCESS = 71
|
||||||
val Shadowsocks = 3
|
const val MSG_MEASURE_CONFIG_CANCEL = 72
|
||||||
val Socks = 4
|
}
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -17,12 +17,16 @@ data class AngConfig(
|
|||||||
var requestHost: String = "",
|
var requestHost: String = "",
|
||||||
var path: String = "",
|
var path: String = "",
|
||||||
var streamSecurity: String = "",
|
var streamSecurity: String = "",
|
||||||
|
var allowInsecure: String = "",
|
||||||
var configType: Int = 1,
|
var configType: Int = 1,
|
||||||
var configVersion: Int = 1,
|
var configVersion: Int = 1,
|
||||||
var testResult: String = "",
|
var testResult: String = "",
|
||||||
var subid: String = "")
|
var subid: String = "",
|
||||||
|
var flow: String = "",
|
||||||
|
var sni: String = "")
|
||||||
|
|
||||||
data class SubItemBean(var id: String = "",
|
data class SubItemBean(var id: String = "",
|
||||||
var remarks: String = "",
|
var remarks: String = "",
|
||||||
var url: String = "")
|
var url: String = "",
|
||||||
|
var enabled: Boolean = true)
|
||||||
}
|
}
|
||||||
|
|||||||
14
V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/EConfigType.kt
Normal file
14
V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/EConfigType.kt
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
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://");
|
||||||
|
|
||||||
|
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 ->
|
||||||
|
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()) {
|
||||||
|
}
|
||||||
@@ -1,142 +1,452 @@
|
|||||||
package com.v2ray.ang.dto
|
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(
|
data class V2rayConfig(
|
||||||
val stats: Any?=null,
|
var stats: Any? = null,
|
||||||
val log: LogBean,
|
val log: LogBean,
|
||||||
val policy: PolicyBean,
|
var policy: PolicyBean?,
|
||||||
val inbounds: ArrayList<InboundBean>,
|
val inbounds: ArrayList<InboundBean>,
|
||||||
var outbounds: ArrayList<OutboundBean>,
|
var outbounds: ArrayList<OutboundBean>,
|
||||||
var dns: DnsBean,
|
var dns: DnsBean,
|
||||||
val routing: RoutingBean) {
|
val routing: RoutingBean,
|
||||||
|
val api: Any? = null,
|
||||||
|
val 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 DEFAULT_FLOW = "xtls-rprx-splice"
|
||||||
|
|
||||||
|
const val TLS = "tls"
|
||||||
|
const val XTLS = "xtls"
|
||||||
|
const val HTTP = "http"
|
||||||
|
}
|
||||||
|
|
||||||
data class LogBean(val access: String,
|
data class LogBean(val access: String,
|
||||||
val error: String,
|
val error: String,
|
||||||
val loglevel: String)
|
var loglevel: String?,
|
||||||
|
val dnsLog: Boolean? = null)
|
||||||
|
|
||||||
data class InboundBean(
|
data class InboundBean(
|
||||||
var tag: String,
|
var tag: String,
|
||||||
var port: Int,
|
var port: Int,
|
||||||
var protocol: String,
|
var protocol: String,
|
||||||
var listen: String?=null,
|
var listen: String? = null,
|
||||||
val settings: InSettingsBean,
|
val settings: Any? = null,
|
||||||
val sniffing: SniffingBean?) {
|
val sniffing: SniffingBean?,
|
||||||
|
val streamSettings: Any? = null,
|
||||||
|
val allocate: Any? = null) {
|
||||||
|
|
||||||
data class InSettingsBean(val auth: String? = null,
|
data class InSettingsBean(val auth: String? = null,
|
||||||
val udp: Boolean? = null,
|
val udp: Boolean? = null,
|
||||||
val userLevel: Int? =null,
|
val userLevel: Int? = null,
|
||||||
val address: String? = null,
|
val address: String? = null,
|
||||||
val port: Int? = null,
|
val port: Int? = null,
|
||||||
val network: String? = null)
|
val network: String? = null)
|
||||||
|
|
||||||
data class SniffingBean(var enabled: Boolean,
|
data class SniffingBean(var enabled: Boolean,
|
||||||
val destOverride: List<String>)
|
val destOverride: ArrayList<String>,
|
||||||
|
val metadataOnly: Boolean? = null)
|
||||||
}
|
}
|
||||||
|
|
||||||
data class OutboundBean(val tag: String,
|
data class OutboundBean(val tag: String = "proxy",
|
||||||
var protocol: String,
|
var protocol: String,
|
||||||
var settings: OutSettingsBean?,
|
var settings: OutSettingsBean? = null,
|
||||||
var streamSettings: StreamSettingsBean?,
|
var streamSettings: StreamSettingsBean? = null,
|
||||||
var mux: MuxBean?) {
|
val proxySettings: Any? = null,
|
||||||
|
val sendThrough: String? = null,
|
||||||
|
val mux: MuxBean? = MuxBean(false)) {
|
||||||
|
|
||||||
data class OutSettingsBean(var vnext: List<VnextBean>?,
|
data class OutSettingsBean(var vnext: List<VnextBean>? = null,
|
||||||
var servers: List<ServersBean>?,
|
var servers: List<ServersBean>? = null,
|
||||||
var response: Response) {
|
/*Blackhole*/
|
||||||
|
var response: Response? = null,
|
||||||
|
/*DNS*/
|
||||||
|
val network: String? = null,
|
||||||
|
val address: String? = null,
|
||||||
|
val port: Int? = null,
|
||||||
|
/*Freedom*/
|
||||||
|
var domainStrategy: String? = null,
|
||||||
|
val redirect: String? = null,
|
||||||
|
val userLevel: Int? = null,
|
||||||
|
/*Loopback*/
|
||||||
|
val inboundTag: String? = null) {
|
||||||
|
|
||||||
data class VnextBean(var address: String,
|
data class VnextBean(var address: String = "",
|
||||||
var port: Int,
|
var port: Int = DEFAULT_PORT,
|
||||||
var users: List<UsersBean>) {
|
var users: List<UsersBean>) {
|
||||||
|
|
||||||
data class UsersBean(var id: String,
|
data class UsersBean(var id: String = "",
|
||||||
var alterId: Int,
|
var alterId: Int? = null,
|
||||||
var security: String,
|
var security: String = DEFAULT_SECURITY,
|
||||||
var level: Int)
|
var level: Int = DEFAULT_LEVEL,
|
||||||
|
var encryption: String = "",
|
||||||
|
var flow: String = "")
|
||||||
}
|
}
|
||||||
|
|
||||||
data class ServersBean(var address: String,
|
data class ServersBean(var address: String = "",
|
||||||
var method: String,
|
var method: String = "chacha20-poly1305",
|
||||||
var ota: Boolean,
|
var ota: Boolean = false,
|
||||||
var password: String,
|
var password: String = "",
|
||||||
var port: Int,
|
var port: Int = DEFAULT_PORT,
|
||||||
var level: Int)
|
var level: Int = DEFAULT_LEVEL,
|
||||||
|
val email: String? = null,
|
||||||
|
var flow: String? = null,
|
||||||
|
val ivCheck: Boolean? = null,
|
||||||
|
var users: List<SocksUsersBean>? = null) {
|
||||||
|
|
||||||
|
|
||||||
|
data class SocksUsersBean(var user: String = "",
|
||||||
|
var pass: String = "",
|
||||||
|
var level: Int = DEFAULT_LEVEL)
|
||||||
|
}
|
||||||
|
|
||||||
data class Response(var type: String)
|
data class Response(var type: String)
|
||||||
}
|
}
|
||||||
|
|
||||||
data class StreamSettingsBean(var network: String,
|
data class StreamSettingsBean(var network: String = DEFAULT_NETWORK,
|
||||||
var security: String,
|
var security: String = "",
|
||||||
var tcpSettings: TcpsettingsBean?,
|
var tcpSettings: TcpSettingsBean? = null,
|
||||||
var kcpsettings: KcpsettingsBean?,
|
var kcpSettings: KcpSettingsBean? = null,
|
||||||
var wssettings: WssettingsBean?,
|
var wsSettings: WsSettingsBean? = null,
|
||||||
var httpsettings: HttpsettingsBean?,
|
var httpSettings: HttpSettingsBean? = null,
|
||||||
var tlssettings: TlssettingsBean?,
|
var tlsSettings: TlsSettingsBean? = null,
|
||||||
var quicsettings: QuicsettingBean?
|
var quicSettings: QuicSettingBean? = null,
|
||||||
|
var xtlsSettings: TlsSettingsBean? = null,
|
||||||
|
var grpcSettings: GrpcSettingsBean? = null,
|
||||||
|
val dsSettings: Any? = null,
|
||||||
|
val sockopt: Any? = null
|
||||||
) {
|
) {
|
||||||
|
|
||||||
data class TcpsettingsBean(var connectionReuse: Boolean = true,
|
data class TcpSettingsBean(var header: HeaderBean = HeaderBean(),
|
||||||
var header: HeaderBean = HeaderBean()) {
|
val acceptProxyProtocol: Boolean? = null) {
|
||||||
data class HeaderBean(var type: String = "none",
|
data class HeaderBean(var type: String = "none",
|
||||||
var request: Any? = null,
|
var request: RequestBean? = null,
|
||||||
var response: Any? = 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,
|
data class KcpSettingsBean(var mtu: Int = 1350,
|
||||||
var tti: Int = 20,
|
var tti: Int = 50,
|
||||||
var uplinkCapacity: Int = 12,
|
var uplinkCapacity: Int = 12,
|
||||||
var downlinkCapacity: Int = 100,
|
var downlinkCapacity: Int = 100,
|
||||||
var congestion: Boolean = false,
|
var congestion: Boolean = false,
|
||||||
var readBufferSize: Int = 1,
|
var readBufferSize: Int = 1,
|
||||||
var writeBufferSize: Int = 1,
|
var writeBufferSize: Int = 1,
|
||||||
|
var header: HeaderBean = HeaderBean(),
|
||||||
|
var 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()) {
|
var header: HeaderBean = HeaderBean()) {
|
||||||
data class HeaderBean(var type: String = "none")
|
data class HeaderBean(var type: String = "none")
|
||||||
}
|
}
|
||||||
|
|
||||||
data class WssettingsBean(var connectionReuse: Boolean = true,
|
data class GrpcSettingsBean(var serviceName: String = "",
|
||||||
var path: String = "",
|
var multiMode: Boolean? = null)
|
||||||
var headers: HeadersBean = HeadersBean()) {
|
|
||||||
data class HeadersBean(var Host: String = "")
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
data class HttpsettingsBean(var host: List<String> = ArrayList<String>(), var path: String = "")
|
fun populateTlsSettings(streamSecurity: String, allowInsecure: Boolean, sni: String, fingerprint: String?, alpns: String?) {
|
||||||
|
security = streamSecurity
|
||||||
data class TlssettingsBean(var allowInsecure: Boolean = true,
|
val tlsSetting = TlsSettingsBean(
|
||||||
var serverName: String = "")
|
allowInsecure = allowInsecure,
|
||||||
|
serverName = sni,
|
||||||
data class QuicsettingBean(var security: String = "none",
|
fingerprint = fingerprint,
|
||||||
var key: String = "",
|
alpn = if (alpns.isNullOrEmpty()) null else alpns.split(",").map { it.trim() }.filter { it.isNotEmpty() }
|
||||||
var header: HeaderBean = HeaderBean()) {
|
)
|
||||||
data class HeaderBean(var type: String = "none")
|
if (security == TLS) {
|
||||||
|
tlsSettings = tlsSetting
|
||||||
|
xtlsSettings = null
|
||||||
|
} else if (security == XTLS) {
|
||||||
|
tlsSettings = null
|
||||||
|
xtlsSettings = tlsSetting
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
data class MuxBean(var enabled: Boolean)
|
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
|
||||||
|
}
|
||||||
|
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
|
||||||
|
}
|
||||||
|
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
|
||||||
|
}
|
||||||
|
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: List<String>)
|
data class DnsBean(var servers: ArrayList<Any>? = null,
|
||||||
data class DnsBean(var servers: List<Any>?=null,
|
var hosts: Map<String, Any>? = null,
|
||||||
var hosts: Map<String, String>?=null
|
val clientIp: String? = null,
|
||||||
|
val disableCache: Boolean? = null,
|
||||||
|
val queryStrategy: String? = null,
|
||||||
|
val tag: String? = null
|
||||||
) {
|
) {
|
||||||
data class ServersBean(var address: String = "",
|
data class ServersBean(var address: String = "",
|
||||||
var port: Int = 0,
|
var port: Int? = null,
|
||||||
var domains: List<String>?)
|
var domains: List<String>? = null,
|
||||||
|
var expectIPs: List<String>? = null,
|
||||||
|
val clientIp: String? = null)
|
||||||
}
|
}
|
||||||
|
|
||||||
data class RoutingBean(var domainStrategy: String,
|
data class RoutingBean(var domainStrategy: String,
|
||||||
var rules: ArrayList<RulesBean>) {
|
var domainMatcher: String? = null,
|
||||||
|
var rules: ArrayList<RulesBean>,
|
||||||
|
val balancers: List<Any>? = null) {
|
||||||
|
|
||||||
data class RulesBean(var type: String = "",
|
data class RulesBean(var type: String = "",
|
||||||
var ip: ArrayList<String>? = null,
|
var ip: ArrayList<String>? = null,
|
||||||
var domain: ArrayList<String>? = null,
|
var domain: ArrayList<String>? = null,
|
||||||
var outboundTag: String = "",
|
var outboundTag: String = "",
|
||||||
|
var balancerTag: String? = null,
|
||||||
var port: String? = null,
|
var port: String? = null,
|
||||||
var inboundTag: ArrayList<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>,
|
data class PolicyBean(var levels: Map<String, LevelBean>,
|
||||||
var system: Any?=null) {
|
var system: Any? = null) {
|
||||||
data class LevelBean(
|
data class LevelBean(
|
||||||
var handshake: Int? = null,
|
var handshake: Int? = null,
|
||||||
var connIdle: Int? = null,
|
var connIdle: Int? = null,
|
||||||
var uplinkOnly: Int? = null,
|
var uplinkOnly: Int? = null,
|
||||||
var downlinkOnly: Int? = null)
|
var downlinkOnly: Int? = null,
|
||||||
|
val statsUserUplink: Boolean? = null,
|
||||||
|
val 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 ->
|
||||||
|
if (outbound.protocol.equals(EConfigType.VMESS.name, true) ||
|
||||||
|
outbound.protocol.equals(EConfigType.VLESS.name, true) ||
|
||||||
|
outbound.protocol.equals(EConfigType.SHADOWSOCKS.name, true) ||
|
||||||
|
outbound.protocol.equals(EConfigType.SOCKS.name, true) ||
|
||||||
|
outbound.protocol.equals(EConfigType.TROJAN.name, true)) {
|
||||||
|
return outbound
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,9 +5,12 @@ data class VmessQRCode(var v: String = "",
|
|||||||
var add: String = "",
|
var add: String = "",
|
||||||
var port: String = "",
|
var port: String = "",
|
||||||
var id: String = "",
|
var id: String = "",
|
||||||
var aid: String = "",
|
var aid: String = "0",
|
||||||
|
var scy: String = "",
|
||||||
var net: String = "",
|
var net: String = "",
|
||||||
var type: String = "",
|
var type: String = "",
|
||||||
var host: String = "",
|
var host: String = "",
|
||||||
var path: String = "",
|
var path: String = "",
|
||||||
var tls: String = "")
|
var tls: String = "",
|
||||||
|
var sni: String = "",
|
||||||
|
var alpn: String = "")
|
||||||
@@ -1,244 +0,0 @@
|
|||||||
package com.v2ray.ang.extension
|
|
||||||
|
|
||||||
import android.app.Fragment
|
|
||||||
import android.app.ProgressDialog
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.DialogInterface
|
|
||||||
import android.database.Cursor
|
|
||||||
import android.graphics.drawable.Drawable
|
|
||||||
import android.support.v7.app.AlertDialog
|
|
||||||
import android.view.KeyEvent
|
|
||||||
import android.view.View
|
|
||||||
import android.widget.ListAdapter
|
|
||||||
|
|
||||||
|
|
||||||
fun Context.alertView(
|
|
||||||
title: String? = null,
|
|
||||||
view: View,
|
|
||||||
init: (KAlertDialogBuilder.() -> Unit)? = null
|
|
||||||
) = KAlertDialogBuilder(this).apply {
|
|
||||||
if (title != null) title(title)
|
|
||||||
if (title != null) customView(view)
|
|
||||||
if (init != null) init()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun Fragment.alert(
|
|
||||||
message: String,
|
|
||||||
title: String? = null,
|
|
||||||
init: (KAlertDialogBuilder.() -> Unit)? = null
|
|
||||||
) = activity.alert(message, title, init)
|
|
||||||
|
|
||||||
fun Context.alert(
|
|
||||||
message: String,
|
|
||||||
title: String? = null,
|
|
||||||
init: (KAlertDialogBuilder.() -> Unit)? = null
|
|
||||||
) = KAlertDialogBuilder(this).apply {
|
|
||||||
if (title != null) title(title)
|
|
||||||
message(message)
|
|
||||||
if (init != null) init()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun Fragment.alert(
|
|
||||||
message: Int,
|
|
||||||
title: Int? = null,
|
|
||||||
init: (KAlertDialogBuilder.() -> Unit)? = null
|
|
||||||
) = activity.alert(message, title, init)
|
|
||||||
|
|
||||||
fun Context.alert(
|
|
||||||
message: Int,
|
|
||||||
title: Int? = null,
|
|
||||||
init: (KAlertDialogBuilder.() -> Unit)? = null
|
|
||||||
) = KAlertDialogBuilder(this).apply {
|
|
||||||
if (title != null) title(title)
|
|
||||||
message(message)
|
|
||||||
if (init != null) init()
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
fun Fragment.alert(init: KAlertDialogBuilder.() -> Unit): KAlertDialogBuilder = activity.alert(init)
|
|
||||||
|
|
||||||
fun Context.alert(init: KAlertDialogBuilder.() -> Unit) = KAlertDialogBuilder(this).apply { init() }
|
|
||||||
|
|
||||||
fun Fragment.progressDialog(
|
|
||||||
message: Int? = null,
|
|
||||||
title: Int? = null,
|
|
||||||
init: (ProgressDialog.() -> Unit)? = null
|
|
||||||
) = activity.progressDialog(message, title, init)
|
|
||||||
|
|
||||||
fun Context.progressDialog(
|
|
||||||
message: Int? = null,
|
|
||||||
title: Int? = null,
|
|
||||||
init: (ProgressDialog.() -> Unit)? = null
|
|
||||||
) = progressDialog(false, message?.let { getString(it) }, title?.let { getString(it) }, init)
|
|
||||||
|
|
||||||
fun Fragment.indeterminateProgressDialog(
|
|
||||||
message: Int? = null,
|
|
||||||
title: Int? = null,
|
|
||||||
init: (ProgressDialog.() -> Unit)? = null
|
|
||||||
) = activity.progressDialog(message, title, init)
|
|
||||||
|
|
||||||
fun Context.indeterminateProgressDialog(
|
|
||||||
message: Int? = null,
|
|
||||||
title: Int? = null,
|
|
||||||
init: (ProgressDialog.() -> Unit)? = null
|
|
||||||
) = progressDialog(true, message?.let { getString(it) }, title?.let { getString(it) }, init)
|
|
||||||
|
|
||||||
fun Fragment.progressDialog(
|
|
||||||
message: String? = null,
|
|
||||||
title: String? = null,
|
|
||||||
init: (ProgressDialog.() -> Unit)? = null
|
|
||||||
) = activity.progressDialog(message, title, init)
|
|
||||||
|
|
||||||
fun Context.progressDialog(
|
|
||||||
message: String? = null,
|
|
||||||
title: String? = null,
|
|
||||||
init: (ProgressDialog.() -> Unit)? = null
|
|
||||||
) = progressDialog(false, message, title, init)
|
|
||||||
|
|
||||||
fun Fragment.indeterminateProgressDialog(
|
|
||||||
message: String? = null,
|
|
||||||
title: String? = null,
|
|
||||||
init: (ProgressDialog.() -> Unit)? = null
|
|
||||||
) = activity.indeterminateProgressDialog(message, title, init)
|
|
||||||
|
|
||||||
fun Context.indeterminateProgressDialog(
|
|
||||||
message: String? = null,
|
|
||||||
title: String? = null,
|
|
||||||
init: (ProgressDialog.() -> Unit)? = null
|
|
||||||
) = progressDialog(true, message, title, init)
|
|
||||||
|
|
||||||
private fun Context.progressDialog(
|
|
||||||
indeterminate: Boolean,
|
|
||||||
message: String? = null,
|
|
||||||
title: String? = null,
|
|
||||||
init: (ProgressDialog.() -> Unit)? = null
|
|
||||||
) = ProgressDialog(this).apply {
|
|
||||||
isIndeterminate = indeterminate
|
|
||||||
if (!indeterminate) setProgressStyle(ProgressDialog.STYLE_HORIZONTAL)
|
|
||||||
if (message != null) setMessage(message)
|
|
||||||
if (title != null) setTitle(title)
|
|
||||||
if (init != null) init()
|
|
||||||
show()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun Fragment.selector(
|
|
||||||
title: CharSequence? = null,
|
|
||||||
items: List<CharSequence>,
|
|
||||||
onClick: (Int) -> Unit
|
|
||||||
): Unit = activity.selector(title, items, onClick)
|
|
||||||
|
|
||||||
fun Context.selector(
|
|
||||||
title: CharSequence? = null,
|
|
||||||
items: List<CharSequence>,
|
|
||||||
onClick: (Int) -> Unit
|
|
||||||
) {
|
|
||||||
with(KAlertDialogBuilder(this)) {
|
|
||||||
if (title != null) title(title)
|
|
||||||
items(items, onClick)
|
|
||||||
show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class KAlertDialogBuilder(val ctx: Context) {
|
|
||||||
|
|
||||||
val builder: AlertDialog.Builder = AlertDialog.Builder(ctx)
|
|
||||||
protected var dialog: AlertDialog? = null
|
|
||||||
|
|
||||||
fun dismiss() {
|
|
||||||
dialog?.dismiss()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun show(): KAlertDialogBuilder {
|
|
||||||
dialog = builder.create()
|
|
||||||
dialog!!.show()
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
fun title(title: CharSequence) {
|
|
||||||
builder.setTitle(title)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun title(resource: Int) {
|
|
||||||
builder.setTitle(resource)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun message(title: CharSequence) {
|
|
||||||
builder.setMessage(title)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun message(resource: Int) {
|
|
||||||
builder.setMessage(resource)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun icon(icon: Int) {
|
|
||||||
builder.setIcon(icon)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun icon(icon: Drawable) {
|
|
||||||
builder.setIcon(icon)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun customTitle(title: View) {
|
|
||||||
builder.setCustomTitle(title)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun customView(view: View) {
|
|
||||||
builder.setView(view)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun cancellable(value: Boolean = true) {
|
|
||||||
builder.setCancelable(value)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onCancel(f: () -> Unit) {
|
|
||||||
builder.setOnCancelListener { f() }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onKey(f: (keyCode: Int, e: KeyEvent) -> Boolean) {
|
|
||||||
builder.setOnKeyListener({ dialog, keyCode, event -> f(keyCode, event) })
|
|
||||||
}
|
|
||||||
|
|
||||||
fun neutralButton(textResource: Int = android.R.string.ok, f: DialogInterface.() -> Unit = { dismiss() }) {
|
|
||||||
neutralButton(ctx.getString(textResource), f)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun neutralButton(title: String, f: DialogInterface.() -> Unit = { dismiss() }) {
|
|
||||||
builder.setNeutralButton(title, { dialog, which -> dialog.f() })
|
|
||||||
}
|
|
||||||
|
|
||||||
fun positiveButton(textResource: Int = android.R.string.ok, f: DialogInterface.() -> Unit) {
|
|
||||||
positiveButton(ctx.getString(textResource), f)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun positiveButton(title: String, f: DialogInterface.() -> Unit) {
|
|
||||||
builder.setPositiveButton(title, { dialog, which -> dialog.f() })
|
|
||||||
}
|
|
||||||
|
|
||||||
fun negativeButton(textResource: Int = android.R.string.cancel, f: DialogInterface.() -> Unit = { dismiss() }) {
|
|
||||||
negativeButton(ctx.getString(textResource), f)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun negativeButton(title: String, f: DialogInterface.() -> Unit = { dismiss() }) {
|
|
||||||
builder.setNegativeButton(title, { dialog, which -> dialog.f() })
|
|
||||||
}
|
|
||||||
|
|
||||||
fun items(itemsId: Int, f: (which: Int) -> Unit) {
|
|
||||||
items(ctx.resources!!.getTextArray(itemsId), f)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun items(items: List<CharSequence>, f: (which: Int) -> Unit) {
|
|
||||||
items(items.toTypedArray(), f)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun items(items: Array<CharSequence>, f: (which: Int) -> Unit) {
|
|
||||||
builder.setItems(items, { dialog, which -> f(which) })
|
|
||||||
}
|
|
||||||
|
|
||||||
fun adapter(adapter: ListAdapter, f: (which: Int) -> Unit) {
|
|
||||||
builder.setAdapter(adapter, { dialog, which -> f(which) })
|
|
||||||
}
|
|
||||||
|
|
||||||
fun adapter(cursor: Cursor, labelColumn: String, f: (which: Int) -> Unit) {
|
|
||||||
builder.setCursor(cursor, { dialog, which -> f(which) }, labelColumn)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -2,9 +2,11 @@ package com.v2ray.ang.extension
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
|
import android.widget.Toast
|
||||||
import com.v2ray.ang.AngApplication
|
import com.v2ray.ang.AngApplication
|
||||||
import me.dozen.dpreference.DPreference
|
import me.drakeet.support.toast.ToastCompat
|
||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
|
import java.net.URI
|
||||||
import java.net.URLConnection
|
import java.net.URLConnection
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -14,11 +16,19 @@ import java.net.URLConnection
|
|||||||
val Context.v2RayApplication: AngApplication
|
val Context.v2RayApplication: AngApplication
|
||||||
get() = applicationContext as AngApplication
|
get() = applicationContext as AngApplication
|
||||||
|
|
||||||
val Context.defaultDPreference: DPreference
|
fun Context.toast(message: Int): Toast = ToastCompat
|
||||||
get() = v2RayApplication.defaultDPreference
|
.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(pair: Pair<String, Any>) = putOpt(pair.first, pair.second)
|
||||||
fun JSONObject.putOpt(pairs: Map<String, Any>) = pairs.forEach { putOpt(it.key to it.value) }
|
fun JSONObject.putOpt(pairs: Map<String, Any>) = pairs.forEach { putOpt(it.key to it.value) }
|
||||||
|
|
||||||
const val threshold = 1000
|
const val threshold = 1000
|
||||||
@@ -27,38 +37,44 @@ const val divisor = 1024F
|
|||||||
fun Long.toSpeedString() = toTrafficString() + "/s"
|
fun Long.toSpeedString() = toTrafficString() + "/s"
|
||||||
|
|
||||||
fun Long.toTrafficString(): String {
|
fun Long.toTrafficString(): String {
|
||||||
|
if (this == 0L)
|
||||||
|
return "\t\t\t0\t B"
|
||||||
|
|
||||||
if (this < threshold)
|
if (this < threshold)
|
||||||
return "$this B"
|
return "${this.toFloat().toShortString()}\t B"
|
||||||
|
|
||||||
val kib = this / divisor
|
val kib = this / divisor
|
||||||
if (kib < threshold)
|
if (kib < threshold)
|
||||||
return "${kib.toShortString()} KB"
|
return "${kib.toShortString()}\t KB"
|
||||||
|
|
||||||
val mib = kib / divisor
|
val mib = kib / divisor
|
||||||
if (mib < threshold)
|
if (mib < threshold)
|
||||||
return "${mib.toShortString()} MB"
|
return "${mib.toShortString()}\t MB"
|
||||||
|
|
||||||
val gib = mib / divisor
|
val gib = mib / divisor
|
||||||
if (gib < threshold)
|
if (gib < threshold)
|
||||||
return "${gib.toShortString()} GB"
|
return "${gib.toShortString()}\t GB"
|
||||||
|
|
||||||
val tib = gib / divisor
|
val tib = gib / divisor
|
||||||
if (tib < threshold)
|
if (tib < threshold)
|
||||||
return "${tib.toShortString()} TB"
|
return "${tib.toShortString()}\t TB"
|
||||||
|
|
||||||
val pib = tib / divisor
|
val pib = tib / divisor
|
||||||
if (pib < threshold)
|
if (pib < threshold)
|
||||||
return "${pib.toShortString()} PB"
|
return "${pib.toShortString()}\t PB"
|
||||||
|
|
||||||
return "∞"
|
return "∞"
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun Float.toShortString(): String {
|
private fun Float.toShortString(): String {
|
||||||
val s = toString()
|
val s = "%.2f".format(this)
|
||||||
if (s.length <= 4)
|
if (s.length <= 4)
|
||||||
return s
|
return s
|
||||||
return s.substring(0, 4).removeSuffix(".")
|
return s.substring(0, 4).removeSuffix(".")
|
||||||
}
|
}
|
||||||
|
|
||||||
val URLConnection.responseLength: Long
|
val URLConnection.responseLength: Long
|
||||||
get() = if (Build.VERSION.SDK_INT >= 24) contentLengthLong else contentLength.toLong()
|
get() = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) contentLengthLong else contentLength.toLong()
|
||||||
|
|
||||||
|
val URI.idnHost: String
|
||||||
|
get() = (host!!).replace("[", "").replace("]", "")
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
package com.v2ray.ang.extension
|
|
||||||
|
|
||||||
import android.preference.Preference
|
|
||||||
|
|
||||||
fun Preference.onClick(listener: () -> Unit) {
|
|
||||||
setOnPreferenceClickListener {
|
|
||||||
listener()
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -5,11 +5,16 @@ import android.content.Context
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.text.TextUtils
|
import android.text.TextUtils
|
||||||
import com.google.zxing.WriterException
|
import com.google.zxing.WriterException
|
||||||
|
import com.tencent.mmkv.MMKV
|
||||||
import com.v2ray.ang.AppConfig
|
import com.v2ray.ang.AppConfig
|
||||||
|
import com.v2ray.ang.service.V2RayServiceManager
|
||||||
|
import com.v2ray.ang.util.MmkvManager
|
||||||
|
|
||||||
import com.v2ray.ang.util.Utils
|
import com.v2ray.ang.util.Utils
|
||||||
|
|
||||||
class TaskerReceiver : BroadcastReceiver() {
|
class TaskerReceiver : BroadcastReceiver() {
|
||||||
|
private val mainStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_MAIN, MMKV.MULTI_PROCESS_MODE) }
|
||||||
|
|
||||||
override fun onReceive(context: Context, intent: Intent?) {
|
override fun onReceive(context: Context, intent: Intent?) {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -21,9 +26,10 @@ class TaskerReceiver : BroadcastReceiver() {
|
|||||||
return
|
return
|
||||||
} else if (switch) {
|
} else if (switch) {
|
||||||
if (guid == AppConfig.TASKER_DEFAULT_GUID) {
|
if (guid == AppConfig.TASKER_DEFAULT_GUID) {
|
||||||
Utils.startVService(context)
|
Utils.startVServiceFromToggle(context)
|
||||||
} else {
|
} else {
|
||||||
Utils.startVService(context, guid)
|
mainStorage?.encode(MmkvManager.KEY_SELECTED_SERVER, guid)
|
||||||
|
V2RayServiceManager.startV2Ray(context)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Utils.stopVService(context)
|
Utils.stopVService(context)
|
||||||
|
|||||||
@@ -3,14 +3,15 @@ package com.v2ray.ang.receiver
|
|||||||
import android.app.PendingIntent
|
import android.app.PendingIntent
|
||||||
import android.appwidget.AppWidgetManager
|
import android.appwidget.AppWidgetManager
|
||||||
import android.appwidget.AppWidgetProvider
|
import android.appwidget.AppWidgetProvider
|
||||||
|
import android.content.ComponentName
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Build
|
||||||
import android.widget.RemoteViews
|
import android.widget.RemoteViews
|
||||||
import com.v2ray.ang.R
|
import com.v2ray.ang.R
|
||||||
import com.v2ray.ang.AppConfig
|
import com.v2ray.ang.AppConfig
|
||||||
|
import com.v2ray.ang.service.V2RayServiceManager
|
||||||
import com.v2ray.ang.util.Utils
|
import com.v2ray.ang.util.Utils
|
||||||
import org.jetbrains.anko.toast
|
|
||||||
|
|
||||||
class WidgetProvider : AppWidgetProvider() {
|
class WidgetProvider : AppWidgetProvider() {
|
||||||
/**
|
/**
|
||||||
@@ -18,11 +19,37 @@ class WidgetProvider : AppWidgetProvider() {
|
|||||||
*/
|
*/
|
||||||
override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) {
|
override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) {
|
||||||
super.onUpdate(context, appWidgetManager, appWidgetIds)
|
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 remoteViews = RemoteViews(context.packageName, R.layout.widget_switch)
|
||||||
val intent = Intent(AppConfig.BROADCAST_ACTION_WIDGET_CLICK)
|
val intent = Intent(context, WidgetProvider::class.java)
|
||||||
val pendingIntent = PendingIntent.getBroadcast(context, R.id.layout_switch, intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
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)
|
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) {
|
for (appWidgetId in appWidgetIds) {
|
||||||
appWidgetManager.updateAppWidget(appWidgetId, remoteViews)
|
appWidgetManager.updateAppWidget(appWidgetId, remoteViews)
|
||||||
@@ -30,21 +57,29 @@ class WidgetProvider : AppWidgetProvider() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 接收窗口小部件点击时发送的广播
|
* 接收窗口小部件发送的广播
|
||||||
*/
|
*/
|
||||||
override fun onReceive(context: Context, intent: Intent) {
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
super.onReceive(context, intent)
|
super.onReceive(context, intent)
|
||||||
if (AppConfig.BROADCAST_ACTION_WIDGET_CLICK == intent.action) {
|
if (AppConfig.BROADCAST_ACTION_WIDGET_CLICK == intent.action) {
|
||||||
|
if (V2RayServiceManager.v2rayPoint.isRunning) {
|
||||||
val isRunning = Utils.isServiceRun(context, "com.v2ray.ang.service.V2RayVpnService")
|
|
||||||
if (isRunning) {
|
|
||||||
// context.toast(R.string.toast_services_stop)
|
|
||||||
Utils.stopVService(context)
|
Utils.stopVService(context)
|
||||||
} else {
|
} else {
|
||||||
// context.toast(R.string.toast_services_start)
|
Utils.startVServiceFromToggle(context)
|
||||||
Utils.startVService(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)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,19 +6,15 @@ import android.content.Context
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.IntentFilter
|
import android.content.IntentFilter
|
||||||
import android.graphics.drawable.Icon
|
import android.graphics.drawable.Icon
|
||||||
import android.net.VpnService
|
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.service.quicksettings.Tile
|
import android.service.quicksettings.Tile
|
||||||
import android.service.quicksettings.TileService
|
import android.service.quicksettings.TileService
|
||||||
import com.v2ray.ang.AppConfig
|
import com.v2ray.ang.AppConfig
|
||||||
import com.v2ray.ang.R
|
import com.v2ray.ang.R
|
||||||
import com.v2ray.ang.extension.defaultDPreference
|
|
||||||
import com.v2ray.ang.util.MessageUtil
|
import com.v2ray.ang.util.MessageUtil
|
||||||
import com.v2ray.ang.util.Utils
|
import com.v2ray.ang.util.Utils
|
||||||
import org.jetbrains.anko.toast
|
|
||||||
import java.lang.ref.SoftReference
|
import java.lang.ref.SoftReference
|
||||||
|
|
||||||
|
|
||||||
@TargetApi(Build.VERSION_CODES.N)
|
@TargetApi(Build.VERSION_CODES.N)
|
||||||
class QSTileService : TileService() {
|
class QSTileService : TileService() {
|
||||||
|
|
||||||
@@ -26,14 +22,13 @@ class QSTileService : TileService() {
|
|||||||
if (state == Tile.STATE_INACTIVE) {
|
if (state == Tile.STATE_INACTIVE) {
|
||||||
qsTile?.state = Tile.STATE_INACTIVE
|
qsTile?.state = Tile.STATE_INACTIVE
|
||||||
qsTile?.label = getString(R.string.app_name)
|
qsTile?.label = getString(R.string.app_name)
|
||||||
qsTile?.icon = Icon.createWithResource(applicationContext, R.drawable.ic_v_idle)
|
qsTile?.icon = Icon.createWithResource(applicationContext, R.drawable.ic_stat_name)
|
||||||
} else if (state == Tile.STATE_ACTIVE) {
|
} else if (state == Tile.STATE_ACTIVE) {
|
||||||
qsTile?.state = Tile.STATE_ACTIVE
|
qsTile?.state = Tile.STATE_ACTIVE
|
||||||
qsTile?.label = defaultDPreference.getPrefString(AppConfig.PREF_CURR_CONFIG_NAME, "NG")
|
qsTile?.label = V2RayServiceManager.currentConfig?.remarks
|
||||||
qsTile?.icon = Icon.createWithResource(applicationContext, R.drawable.ic_v)
|
qsTile?.icon = Icon.createWithResource(applicationContext, R.drawable.ic_stat_name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
qsTile?.updateTile()
|
qsTile?.updateTile()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,11 +51,7 @@ class QSTileService : TileService() {
|
|||||||
super.onClick()
|
super.onClick()
|
||||||
when (qsTile.state) {
|
when (qsTile.state) {
|
||||||
Tile.STATE_INACTIVE -> {
|
Tile.STATE_INACTIVE -> {
|
||||||
val intent = VpnService.prepare(this)
|
Utils.startVServiceFromToggle(this)
|
||||||
if (intent == null)
|
|
||||||
if (!Utils.startVService(this)) {
|
|
||||||
toast(R.string.app_tile_first_use)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Tile.STATE_ACTIVE -> {
|
Tile.STATE_ACTIVE -> {
|
||||||
Utils.stopVService(this)
|
Utils.stopVService(this)
|
||||||
@@ -93,4 +84,4 @@ class QSTileService : TileService() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,134 +1,106 @@
|
|||||||
package com.v2ray.ang.service
|
package com.v2ray.ang.service
|
||||||
|
|
||||||
import android.app.Notification
|
import android.app.Service
|
||||||
import android.app.NotificationChannel
|
|
||||||
import android.app.NotificationManager
|
|
||||||
import android.app.PendingIntent
|
|
||||||
import android.content.BroadcastReceiver
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.IntentFilter
|
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.graphics.Color
|
|
||||||
import android.net.*
|
import android.net.*
|
||||||
import android.net.VpnService
|
import android.os.Build
|
||||||
import android.os.*
|
import android.os.ParcelFileDescriptor
|
||||||
import android.support.annotation.RequiresApi
|
import android.os.StrictMode
|
||||||
import android.support.v4.app.NotificationCompat
|
import android.util.Log
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
|
import com.tencent.mmkv.MMKV
|
||||||
import com.v2ray.ang.AppConfig
|
import com.v2ray.ang.AppConfig
|
||||||
import com.v2ray.ang.R
|
import com.v2ray.ang.R
|
||||||
import com.v2ray.ang.extension.defaultDPreference
|
import com.v2ray.ang.dto.ERoutingMode
|
||||||
import com.v2ray.ang.extension.toSpeedString
|
import com.v2ray.ang.util.MmkvManager
|
||||||
import com.v2ray.ang.ui.MainActivity
|
import com.v2ray.ang.util.MyContextWrapper
|
||||||
import com.v2ray.ang.ui.PerAppProxyActivity
|
|
||||||
import com.v2ray.ang.ui.SettingsActivity
|
|
||||||
import com.v2ray.ang.util.MessageUtil
|
|
||||||
import com.v2ray.ang.util.Utils
|
import com.v2ray.ang.util.Utils
|
||||||
import libv2ray.Libv2ray
|
import kotlinx.coroutines.Dispatchers
|
||||||
import libv2ray.V2RayVPNServiceSupportsSet
|
import kotlinx.coroutines.GlobalScope
|
||||||
import rx.Observable
|
import kotlinx.coroutines.launch
|
||||||
import rx.Subscription
|
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.lang.ref.SoftReference
|
import java.lang.ref.SoftReference
|
||||||
import android.os.Build
|
|
||||||
import android.util.Log
|
|
||||||
import go.Seq
|
|
||||||
import org.jetbrains.anko.doAsync
|
|
||||||
|
|
||||||
class V2RayVpnService : VpnService() {
|
class V2RayVpnService : VpnService(), ServiceControl {
|
||||||
companion object {
|
companion object {
|
||||||
const val NOTIFICATION_ID = 1
|
private const val VPN_MTU = 1500
|
||||||
const val NOTIFICATION_PENDING_INTENT_CONTENT = 0
|
private const val PRIVATE_VLAN4_CLIENT = "26.26.26.1"
|
||||||
const val NOTIFICATION_PENDING_INTENT_STOP_V2RAY = 1
|
private const val PRIVATE_VLAN4_ROUTER = "26.26.26.2"
|
||||||
|
private const val PRIVATE_VLAN6_CLIENT = "da26:2626::1"
|
||||||
fun startV2Ray(context: Context) {
|
private const val PRIVATE_VLAN6_ROUTER = "da26:2626::2"
|
||||||
val intent = Intent(context.applicationContext, V2RayVpnService::class.java)
|
private const val TUN2SOCKS = "libtun2socks.so"
|
||||||
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.N_MR1) {
|
|
||||||
context.startForegroundService(intent)
|
|
||||||
} else {
|
|
||||||
context.startService(intent)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private val v2rayPoint = Libv2ray.newV2RayPoint(V2RayCallback())
|
private val settingsStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_SETTING, MMKV.MULTI_PROCESS_MODE) }
|
||||||
private lateinit var configContent: String
|
|
||||||
private lateinit var mInterface: ParcelFileDescriptor
|
private lateinit var mInterface: ParcelFileDescriptor
|
||||||
val fd: Int get() = mInterface.fd
|
|
||||||
private var mBuilder: NotificationCompat.Builder? = null
|
|
||||||
private var mSubscription: Subscription? = null
|
|
||||||
private var mNotificationManager: NotificationManager? = null
|
|
||||||
|
|
||||||
|
//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
|
* 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
|
* 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
|
* 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.
|
* 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
|
* 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 {
|
private val defaultNetworkRequest by lazy {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
NetworkRequest.Builder()
|
||||||
NetworkRequest.Builder()
|
|
||||||
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
|
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
|
||||||
.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED)
|
.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED)
|
||||||
.build()
|
.build()
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private val connectivity by lazy { getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager }
|
private val connectivity by lazy { getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager }
|
||||||
|
|
||||||
|
@delegate:RequiresApi(Build.VERSION_CODES.P)
|
||||||
private val defaultNetworkCallback by lazy {
|
private val defaultNetworkCallback by lazy {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
object : ConnectivityManager.NetworkCallback() {
|
||||||
object : ConnectivityManager.NetworkCallback() {
|
override fun onAvailable(network: Network) {
|
||||||
override fun onAvailable(network: Network) {
|
setUnderlyingNetworks(arrayOf(network))
|
||||||
setUnderlyingNetworks(arrayOf(network))
|
}
|
||||||
}
|
|
||||||
override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities?) {
|
override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) {
|
||||||
// it's a good idea to refresh capabilities
|
// it's a good idea to refresh capabilities
|
||||||
setUnderlyingNetworks(arrayOf(network))
|
setUnderlyingNetworks(arrayOf(network))
|
||||||
}
|
}
|
||||||
override fun onLost(network: Network) {
|
|
||||||
setUnderlyingNetworks(null)
|
override fun onLost(network: Network) {
|
||||||
}
|
setUnderlyingNetworks(null)
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
private var listeningForDefaultNetwork = false
|
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
|
|
||||||
val policy = StrictMode.ThreadPolicy.Builder().permitAll().build()
|
val policy = StrictMode.ThreadPolicy.Builder().permitAll().build()
|
||||||
StrictMode.setThreadPolicy(policy)
|
StrictMode.setThreadPolicy(policy)
|
||||||
v2rayPoint.packageName = Utils.packagePath(applicationContext)
|
V2RayServiceManager.serviceControl = SoftReference(this)
|
||||||
Seq.setContext(applicationContext)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onRevoke() {
|
override fun onRevoke() {
|
||||||
stopV2Ray()
|
stopV2Ray()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onLowMemory() {
|
// override fun onLowMemory() {
|
||||||
stopV2Ray()
|
// stopV2Ray()
|
||||||
super.onLowMemory()
|
// super.onLowMemory()
|
||||||
}
|
// }
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
cancelNotification()
|
V2RayServiceManager.cancelNotification()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setup(parameters: String) {
|
private fun setup() {
|
||||||
|
val prepare = prepare(this)
|
||||||
val prepare = VpnService.prepare(this)
|
|
||||||
if (prepare != null) {
|
if (prepare != null) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -136,33 +108,47 @@ class V2RayVpnService : VpnService() {
|
|||||||
// If the old interface has exactly the same parameters, use it!
|
// If the old interface has exactly the same parameters, use it!
|
||||||
// Configure a builder while parsing the parameters.
|
// Configure a builder while parsing the parameters.
|
||||||
val builder = Builder()
|
val builder = Builder()
|
||||||
val enableLocalDns = defaultDPreference.getPrefBoolean(SettingsActivity.PREF_LOCAL_DNS_ENABLED, false)
|
//val enableLocalDns = defaultDPreference.getPrefBoolean(AppConfig.PREF_LOCAL_DNS_ENABLED, false)
|
||||||
|
|
||||||
parameters.split(" ")
|
val routingMode = settingsStorage?.decodeString(AppConfig.PREF_ROUTING_MODE) ?: ERoutingMode.GLOBAL_PROXY.value
|
||||||
.map { it.split(",") }
|
|
||||||
.forEach {
|
|
||||||
when (it[0][0]) {
|
|
||||||
'm' -> builder.setMtu(java.lang.Short.parseShort(it[1]).toInt())
|
|
||||||
's' -> builder.addSearchDomain(it[1])
|
|
||||||
'a' -> builder.addAddress(it[1], Integer.parseInt(it[2]))
|
|
||||||
'r' -> builder.addRoute(it[1], Integer.parseInt(it[2]))
|
|
||||||
'd' -> builder.addDnsServer(it[1])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if(!enableLocalDns) {
|
builder.setMtu(VPN_MTU)
|
||||||
Utils.getRemoteDnsServers(defaultDPreference)
|
builder.addAddress(PRIVATE_VLAN4_CLIENT, 30)
|
||||||
.forEach {
|
//builder.addDnsServer(PRIVATE_VLAN4_ROUTER)
|
||||||
builder.addDnsServer(it)
|
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
builder.setSession(defaultDPreference.getPrefString(AppConfig.PREF_CURR_CONFIG_NAME, ""))
|
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 (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP &&
|
if (settingsStorage?.decodeBool(AppConfig.PREF_LOCAL_DNS_ENABLED) == true) {
|
||||||
defaultDPreference.getPrefBoolean(SettingsActivity.PREF_PER_APP_PROXY, false)) {
|
builder.addDnsServer(PRIVATE_VLAN4_ROUTER)
|
||||||
val apps = defaultDPreference.getPrefStringSet(PerAppProxyActivity.PREF_PER_APP_PROXY_SET, null)
|
} else {
|
||||||
val bypassApps = defaultDPreference.getPrefBoolean(PerAppProxyActivity.PREF_BYPASS_APPS, false)
|
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 {
|
apps?.forEach {
|
||||||
try {
|
try {
|
||||||
if (bypassApps)
|
if (bypassApps)
|
||||||
@@ -179,33 +165,78 @@ class V2RayVpnService : VpnService() {
|
|||||||
try {
|
try {
|
||||||
mInterface.close()
|
mInterface.close()
|
||||||
} catch (ignored: Exception) {
|
} catch (ignored: Exception) {
|
||||||
|
// ignored
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||||
connectivity.requestNetwork(defaultNetworkRequest, defaultNetworkCallback)
|
try {
|
||||||
listeningForDefaultNetwork = true
|
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.
|
// Create a new interface using the builder and save the parameters.
|
||||||
mInterface = builder.establish()
|
try {
|
||||||
sendFd()
|
mInterface = builder.establish()!!
|
||||||
startSpeedNotification()
|
runTun2socks()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// non-nullable lateinit var
|
||||||
|
e.printStackTrace()
|
||||||
|
stopV2Ray()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun shutdown() {
|
private fun runTun2socks() {
|
||||||
stopV2Ray(true)
|
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()
|
||||||
|
Log.d(packageName, process.toString())
|
||||||
|
|
||||||
|
sendFd()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.d(packageName, e.toString())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun sendFd() {
|
private fun sendFd() {
|
||||||
val fd = mInterface.fileDescriptor
|
val fd = mInterface.fileDescriptor
|
||||||
val path = File(Utils.packagePath(applicationContext), "sock_path").absolutePath
|
val path = File(applicationContext.filesDir, "sock_path").absolutePath
|
||||||
|
Log.d(packageName, path)
|
||||||
|
|
||||||
doAsync {
|
GlobalScope.launch(Dispatchers.IO) {
|
||||||
var tries = 0
|
var tries = 0
|
||||||
while (true) try {
|
while (true) try {
|
||||||
Thread.sleep(50L shl tries)
|
Thread.sleep(50L shl tries)
|
||||||
Log.d(packageName, "sendFd tries: " + tries.toString())
|
Log.d(packageName, "sendFd tries: $tries")
|
||||||
LocalSocket().use { localSocket ->
|
LocalSocket().use { localSocket ->
|
||||||
localSocket.connect(LocalSocketAddress(path, LocalSocketAddress.Namespace.FILESYSTEM))
|
localSocket.connect(LocalSocketAddress(path, LocalSocketAddress.Namespace.FILESYSTEM))
|
||||||
localSocket.setFileDescriptorsForSend(arrayOf(fd))
|
localSocket.setFileDescriptorsForSend(arrayOf(fd))
|
||||||
@@ -221,74 +252,34 @@ class V2RayVpnService : VpnService() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
startV2ray()
|
V2RayServiceManager.startV2rayPoint()
|
||||||
return START_STICKY
|
return START_STICKY
|
||||||
//return super.onStartCommand(intent, flags, startId)
|
//return super.onStartCommand(intent, flags, startId)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun startV2ray() {
|
|
||||||
if (!v2rayPoint.isRunning) {
|
|
||||||
|
|
||||||
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)
|
|
||||||
registerReceiver(mMsgReceive, mFilter)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
}
|
|
||||||
|
|
||||||
configContent = defaultDPreference.getPrefString(AppConfig.PREF_CURR_CONFIG, "")
|
|
||||||
v2rayPoint.configureFileContent = configContent
|
|
||||||
v2rayPoint.enableLocalDNS = defaultDPreference.getPrefBoolean(SettingsActivity.PREF_LOCAL_DNS_ENABLED, false)
|
|
||||||
v2rayPoint.forwardIpv6 = defaultDPreference.getPrefBoolean(SettingsActivity.PREF_FORWARD_IPV6, false)
|
|
||||||
v2rayPoint.domainName = defaultDPreference.getPrefString(AppConfig.PREF_CURR_CONFIG_DOMAIN, "")
|
|
||||||
|
|
||||||
try {
|
|
||||||
v2rayPoint.runLoop()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.d(packageName, e.toString())
|
|
||||||
}
|
|
||||||
|
|
||||||
if (v2rayPoint.isRunning) {
|
|
||||||
MessageUtil.sendMsg2UI(this, AppConfig.MSG_STATE_START_SUCCESS, "")
|
|
||||||
showNotification()
|
|
||||||
} else {
|
|
||||||
MessageUtil.sendMsg2UI(this, AppConfig.MSG_STATE_START_FAILURE, "")
|
|
||||||
cancelNotification()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// showNotification()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun stopV2Ray(isForced: Boolean = true) {
|
private fun stopV2Ray(isForced: Boolean = true) {
|
||||||
// val configName = defaultDPreference.getPrefString(PREF_CURR_CONFIG_GUID, "")
|
// val configName = defaultDPreference.getPrefString(PREF_CURR_CONFIG_GUID, "")
|
||||||
// val emptyInfo = VpnNetworkInfo()
|
// val emptyInfo = VpnNetworkInfo()
|
||||||
// val info = loadVpnNetworkInfo(configName, emptyInfo)!! + (lastNetworkInfo ?: emptyInfo)
|
// val info = loadVpnNetworkInfo(configName, emptyInfo)!! + (lastNetworkInfo ?: emptyInfo)
|
||||||
// saveVpnNetworkInfo(configName, info)
|
// saveVpnNetworkInfo(configName, info)
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||||
if (listeningForDefaultNetwork) {
|
|
||||||
connectivity.unregisterNetworkCallback(defaultNetworkCallback)
|
|
||||||
listeningForDefaultNetwork = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (v2rayPoint.isRunning) {
|
|
||||||
try {
|
try {
|
||||||
v2rayPoint.stopLoop()
|
connectivity.unregisterNetworkCallback(defaultNetworkCallback)
|
||||||
} catch (e: Exception) {
|
} catch (ignored: Exception) {
|
||||||
Log.d(packageName, e.toString())
|
// ignored
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
MessageUtil.sendMsg2UI(this, AppConfig.MSG_STATE_STOP_SUCCESS, "")
|
try {
|
||||||
cancelNotification()
|
Log.d(packageName, "tun2socks destroy")
|
||||||
|
process.destroy()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.d(packageName, e.toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
V2RayServiceManager.stopV2rayPoint()
|
||||||
|
|
||||||
if (isForced) {
|
if (isForced) {
|
||||||
try {
|
|
||||||
unregisterReceiver(mMsgReceive)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
}
|
|
||||||
|
|
||||||
//stopSelf has to be called ahead of mInterface.close(). otherwise v2ray core cannot be stooped
|
//stopSelf has to be called ahead of mInterface.close(). otherwise v2ray core cannot be stooped
|
||||||
//It's strage but true.
|
//It's strage but true.
|
||||||
//This can be verified by putting stopself() behind and call stopLoop and startLoop
|
//This can be verified by putting stopself() behind and call stopLoop and startLoop
|
||||||
@@ -299,207 +290,32 @@ class V2RayVpnService : VpnService() {
|
|||||||
try {
|
try {
|
||||||
mInterface.close()
|
mInterface.close()
|
||||||
} catch (ignored: Exception) {
|
} catch (ignored: Exception) {
|
||||||
}
|
// ignored
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun showNotification() {
|
|
||||||
val startMainIntent = Intent(applicationContext, MainActivity::class.java)
|
|
||||||
val contentPendingIntent = PendingIntent.getActivity(applicationContext,
|
|
||||||
NOTIFICATION_PENDING_INTENT_CONTENT, startMainIntent,
|
|
||||||
PendingIntent.FLAG_UPDATE_CURRENT)
|
|
||||||
|
|
||||||
val stopV2RayIntent = Intent(AppConfig.BROADCAST_ACTION_SERVICE)
|
|
||||||
stopV2RayIntent.`package` = AppConfig.ANG_PACKAGE
|
|
||||||
stopV2RayIntent.putExtra("key", AppConfig.MSG_STATE_STOP)
|
|
||||||
|
|
||||||
val stopV2RayPendingIntent = PendingIntent.getBroadcast(applicationContext,
|
|
||||||
NOTIFICATION_PENDING_INTENT_STOP_V2RAY, stopV2RayIntent,
|
|
||||||
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(applicationContext, channelId)
|
|
||||||
.setSmallIcon(R.drawable.ic_v)
|
|
||||||
.setContentTitle(defaultDPreference.getPrefString(AppConfig.PREF_CURR_CONFIG_NAME, ""))
|
|
||||||
.setPriority(NotificationCompat.PRIORITY_MIN)
|
|
||||||
.setOngoing(true)
|
|
||||||
.setShowWhen(false)
|
|
||||||
.setOnlyAlertOnce(true)
|
|
||||||
.setContentIntent(contentPendingIntent)
|
|
||||||
.addAction(R.drawable.ic_close_grey_800_24dp,
|
|
||||||
getString(R.string.notification_action_stop_v2ray),
|
|
||||||
stopV2RayPendingIntent)
|
|
||||||
//.build()
|
|
||||||
|
|
||||||
//mBuilder?.setDefaults(NotificationCompat.FLAG_ONLY_ALERT_ONCE) //取消震动,铃声其他都不好使
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun cancelNotification() {
|
|
||||||
stopForeground(true)
|
|
||||||
mBuilder = null
|
|
||||||
mSubscription?.unsubscribe()
|
|
||||||
mSubscription = null
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun updateNotification(contentText: String) {
|
|
||||||
if (mBuilder != null) {
|
|
||||||
mBuilder?.setContentTitle(contentText)
|
|
||||||
getNotificationManager().notify(NOTIFICATION_ID, mBuilder?.build())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getNotificationManager(): NotificationManager {
|
|
||||||
if (mNotificationManager == null) {
|
|
||||||
mNotificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
|
||||||
}
|
|
||||||
return mNotificationManager!!
|
|
||||||
}
|
|
||||||
|
|
||||||
fun startSpeedNotification() {
|
|
||||||
if (mSubscription == null &&
|
|
||||||
v2rayPoint.isRunning &&
|
|
||||||
defaultDPreference.getPrefBoolean(SettingsActivity.PREF_SPEED_ENABLED, false)) {
|
|
||||||
val cf_name = defaultDPreference.getPrefString(AppConfig.PREF_CURR_CONFIG_NAME, "")
|
|
||||||
var last_zero_speed = false
|
|
||||||
|
|
||||||
mSubscription = Observable.interval(3, java.util.concurrent.TimeUnit.SECONDS)
|
|
||||||
.subscribe {
|
|
||||||
val uplink = v2rayPoint.queryStats("socks", "uplink")
|
|
||||||
val downlink = v2rayPoint.queryStats("socks", "downlink")
|
|
||||||
val zero_speed = (uplink == 0L && downlink == 0L)
|
|
||||||
if (!zero_speed || !last_zero_speed) {
|
|
||||||
updateNotification("${cf_name} • ${(uplink / 3).toSpeedString()}↑ ${(downlink / 3).toSpeedString()}↓")
|
|
||||||
}
|
|
||||||
last_zero_speed = zero_speed
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
fun stopSpeedNotification() {
|
|
||||||
if (mSubscription != null) {
|
|
||||||
mSubscription?.unsubscribe() //stop queryStats
|
|
||||||
mSubscription = null
|
|
||||||
|
|
||||||
val cf_name = defaultDPreference.getPrefString(AppConfig.PREF_CURR_CONFIG_NAME, "")
|
|
||||||
updateNotification(cf_name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private inner class V2RayCallback : V2RayVPNServiceSupportsSet {
|
|
||||||
override fun shutdown(): Long {
|
|
||||||
// called by go
|
|
||||||
// shutdown the whole vpn service
|
|
||||||
try {
|
|
||||||
this@V2RayVpnService.shutdown()
|
|
||||||
return 0
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.d(packageName, e.toString())
|
|
||||||
return -1
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun prepare(): Long {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun protect(l: Long) = (if (this@V2RayVpnService.protect(l.toInt())) 0 else 1).toLong()
|
|
||||||
|
|
||||||
override fun onEmitStatus(l: Long, s: String?): Long {
|
|
||||||
//Logger.d(s)
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun setup(s: String): Long {
|
|
||||||
//Logger.d(s)
|
|
||||||
try {
|
|
||||||
this@V2RayVpnService.setup(s)
|
|
||||||
return 0
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.d(packageName, e.toString())
|
|
||||||
return -1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun sendFd(): Long {
|
|
||||||
try {
|
|
||||||
this@V2RayVpnService.sendFd()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.d(packageName, e.toString())
|
|
||||||
return -1
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private var mMsgReceive = ReceiveMessageHandler(this@V2RayVpnService)
|
override fun getService(): Service {
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
private class ReceiveMessageHandler(vpnService: V2RayVpnService) : BroadcastReceiver() {
|
override fun startService() {
|
||||||
internal var mReference: SoftReference<V2RayVpnService> = SoftReference(vpnService)
|
setup()
|
||||||
|
}
|
||||||
|
|
||||||
override fun onReceive(ctx: Context?, intent: Intent?) {
|
override fun stopService() {
|
||||||
val vpnService = mReference.get()
|
stopV2Ray(true)
|
||||||
when (intent?.getIntExtra("key", 0)) {
|
}
|
||||||
AppConfig.MSG_REGISTER_CLIENT -> {
|
|
||||||
//Logger.e("ReceiveMessageHandler", intent?.getIntExtra("key", 0).toString())
|
|
||||||
|
|
||||||
val isRunning = vpnService?.v2rayPoint!!.isRunning
|
override fun vpnProtect(socket: Int): Boolean {
|
||||||
&& VpnService.prepare(vpnService) == null
|
return protect(socket)
|
||||||
if (isRunning) {
|
}
|
||||||
MessageUtil.sendMsg2UI(vpnService, AppConfig.MSG_STATE_RUNNING, "")
|
|
||||||
} else {
|
|
||||||
MessageUtil.sendMsg2UI(vpnService, AppConfig.MSG_STATE_NOT_RUNNING, "")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
AppConfig.MSG_UNREGISTER_CLIENT -> {
|
|
||||||
// vpnService?.mMsgSend = null
|
|
||||||
}
|
|
||||||
AppConfig.MSG_STATE_START -> {
|
|
||||||
//nothing to do
|
|
||||||
}
|
|
||||||
AppConfig.MSG_STATE_STOP -> {
|
|
||||||
vpnService?.stopV2Ray()
|
|
||||||
}
|
|
||||||
AppConfig.MSG_STATE_RESTART -> {
|
|
||||||
vpnService?.startV2ray()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
when (intent?.action) {
|
@RequiresApi(Build.VERSION_CODES.N)
|
||||||
Intent.ACTION_SCREEN_OFF -> {
|
override fun attachBaseContext(newBase: Context?) {
|
||||||
Log.d(AppConfig.ANG_PACKAGE, "SCREEN_OFF, stop querying stats")
|
val context = newBase?.let {
|
||||||
vpnService?.stopSpeedNotification()
|
MyContextWrapper.wrap(newBase, Utils.getLocale(newBase))
|
||||||
}
|
|
||||||
Intent.ACTION_SCREEN_ON -> {
|
|
||||||
Log.d(AppConfig.ANG_PACKAGE, "SCREEN_ON, start querying stats")
|
|
||||||
vpnService?.startSpeedNotification()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
super.attachBaseContext(context)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,14 @@
|
|||||||
package com.v2ray.ang.ui
|
package com.v2ray.ang.ui
|
||||||
|
|
||||||
import android.support.v7.app.AppCompatActivity
|
import android.content.Context
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Bundle
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import android.view.MenuItem
|
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() {
|
abstract class BaseActivity : AppCompatActivity() {
|
||||||
override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
|
override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
|
||||||
@@ -11,4 +18,36 @@ abstract class BaseActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
else -> super.onOptionsItemSelected(item)
|
else -> super.onOptionsItemSelected(item)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
checkDarkMode()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun checkDarkMode() {
|
||||||
|
if (Utils.getDarkModeStatus(this)) {
|
||||||
|
if (this.javaClass.simpleName == "MainActivity") {
|
||||||
|
setTheme(R.style.AppThemeDark_NoActionBar)
|
||||||
|
} else {
|
||||||
|
setTheme(R.style.AppThemeDark)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (this.javaClass.simpleName == "MainActivity") {
|
||||||
|
setTheme(R.style.AppThemeLight_NoActionBar)
|
||||||
|
} else {
|
||||||
|
setTheme(R.style.AppThemeLight)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.N)
|
||||||
|
override fun attachBaseContext(newBase: Context?) {
|
||||||
|
val context = newBase?.let {
|
||||||
|
MyContextWrapper.wrap(newBase, Utils.getLocale(newBase))
|
||||||
|
}
|
||||||
|
super.attachBaseContext(context)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,206 +0,0 @@
|
|||||||
package com.v2ray.ang.ui
|
|
||||||
|
|
||||||
import android.app.ActivityOptions
|
|
||||||
import android.app.FragmentManager
|
|
||||||
import android.content.Intent
|
|
||||||
import android.content.res.Configuration
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.support.design.widget.NavigationView
|
|
||||||
import android.support.v4.view.GravityCompat
|
|
||||||
import android.support.v4.widget.DrawerLayout
|
|
||||||
import android.support.v7.app.ActionBarDrawerToggle
|
|
||||||
import android.support.v7.widget.Toolbar
|
|
||||||
import android.util.Log
|
|
||||||
import android.view.MenuItem
|
|
||||||
import android.view.View
|
|
||||||
//import com.v2ray.ang.InappBuyActivity
|
|
||||||
|
|
||||||
import com.v2ray.ang.R
|
|
||||||
import org.jetbrains.anko.startActivity
|
|
||||||
|
|
||||||
|
|
||||||
abstract class BaseDrawerActivity : BaseActivity() {
|
|
||||||
companion object {
|
|
||||||
|
|
||||||
private val TAG = "BaseDrawerActivity"
|
|
||||||
}
|
|
||||||
|
|
||||||
private var mToolbar: Toolbar? = null
|
|
||||||
|
|
||||||
private var mDrawerToggle: ActionBarDrawerToggle? = null
|
|
||||||
|
|
||||||
private var mDrawerLayout: DrawerLayout? = null
|
|
||||||
|
|
||||||
private var mToolbarInitialized: Boolean = false
|
|
||||||
|
|
||||||
private var mItemToOpenWhenDrawerCloses = -1
|
|
||||||
|
|
||||||
private val backStackChangedListener = FragmentManager.OnBackStackChangedListener { updateDrawerToggle() }
|
|
||||||
|
|
||||||
private val drawerListener = object : DrawerLayout.DrawerListener {
|
|
||||||
override fun onDrawerSlide(drawerView: View, slideOffset: Float) {
|
|
||||||
mDrawerToggle!!.onDrawerSlide(drawerView, slideOffset)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDrawerOpened(drawerView: View) {
|
|
||||||
mDrawerToggle!!.onDrawerOpened(drawerView)
|
|
||||||
//supportActionBar!!.setTitle(R.string.app_name)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDrawerClosed(drawerView: View) {
|
|
||||||
mDrawerToggle!!.onDrawerClosed(drawerView)
|
|
||||||
|
|
||||||
if (mItemToOpenWhenDrawerCloses >= 0) {
|
|
||||||
val extras = ActivityOptions.makeCustomAnimation(
|
|
||||||
this@BaseDrawerActivity, R.anim.fade_in, R.anim.fade_out).toBundle()
|
|
||||||
var activityClass: Class<*>? = null
|
|
||||||
when (mItemToOpenWhenDrawerCloses) {
|
|
||||||
R.id.server_profile -> activityClass = MainActivity::class.java
|
|
||||||
R.id.sub_setting -> activityClass = SubSettingActivity::class.java
|
|
||||||
R.id.settings -> activityClass = SettingsActivity::class.java
|
|
||||||
R.id.logcat -> {
|
|
||||||
startActivity<LogcatActivity>()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
R.id.donate -> {
|
|
||||||
// startActivity<InappBuyActivity>()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (activityClass != null) {
|
|
||||||
startActivity(Intent(this@BaseDrawerActivity, activityClass), extras)
|
|
||||||
finish()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDrawerStateChanged(newState: Int) {
|
|
||||||
mDrawerToggle!!.onDrawerStateChanged(newState)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
Log.d(TAG, "Activity onCreate")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onStart() {
|
|
||||||
super.onStart()
|
|
||||||
if (!mToolbarInitialized) {
|
|
||||||
throw IllegalStateException("You must run super.initializeToolbar at " + "the end of your onCreate method")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public override fun onResume() {
|
|
||||||
super.onResume()
|
|
||||||
// Whenever the fragment back stack changes, we may need to update the
|
|
||||||
// action bar toggle: only top level screens show the hamburger-like icon, inner
|
|
||||||
// screens - either Activities or fragments - show the "Up" icon instead.
|
|
||||||
fragmentManager.addOnBackStackChangedListener(backStackChangedListener)
|
|
||||||
}
|
|
||||||
|
|
||||||
public override fun onPause() {
|
|
||||||
super.onPause()
|
|
||||||
fragmentManager.removeOnBackStackChangedListener(backStackChangedListener)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPostCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onPostCreate(savedInstanceState)
|
|
||||||
mDrawerToggle!!.syncState()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onConfigurationChanged(newConfig: Configuration) {
|
|
||||||
super.onConfigurationChanged(newConfig)
|
|
||||||
mDrawerToggle!!.onConfigurationChanged(newConfig)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
|
||||||
if (mDrawerToggle != null && mDrawerToggle!!.onOptionsItemSelected(item)) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
// If not handled by drawerToggle, home needs to be handled by returning to previous
|
|
||||||
if (item.itemId == android.R.id.home) {
|
|
||||||
onBackPressed()
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return super.onOptionsItemSelected(item)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onBackPressed() {
|
|
||||||
// If the drawer is open, back will close it
|
|
||||||
if (mDrawerLayout != null && mDrawerLayout!!.isDrawerOpen(GravityCompat.START)) {
|
|
||||||
mDrawerLayout!!.closeDrawers()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// Otherwise, it may return to the previous fragment stack
|
|
||||||
val fragmentManager = fragmentManager
|
|
||||||
if (fragmentManager.backStackEntryCount > 0) {
|
|
||||||
fragmentManager.popBackStack()
|
|
||||||
} else {
|
|
||||||
// Lastly, it will rely on the system behavior for back
|
|
||||||
super.onBackPressed()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun updateDrawerToggle() {
|
|
||||||
if (mDrawerToggle == null) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
val isRoot = fragmentManager.backStackEntryCount == 0
|
|
||||||
mDrawerToggle!!.isDrawerIndicatorEnabled = isRoot
|
|
||||||
|
|
||||||
supportActionBar!!.setDisplayShowHomeEnabled(!isRoot)
|
|
||||||
supportActionBar!!.setDisplayHomeAsUpEnabled(!isRoot)
|
|
||||||
supportActionBar!!.setHomeButtonEnabled(!isRoot)
|
|
||||||
|
|
||||||
if (isRoot) {
|
|
||||||
mDrawerToggle!!.syncState()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected fun initializeToolbar() {
|
|
||||||
mToolbar = findViewById<View>(R.id.toolbar) as Toolbar
|
|
||||||
if (mToolbar == null) {
|
|
||||||
throw IllegalStateException("Layout is required to include a Toolbar with id " + "'toolbar'")
|
|
||||||
}
|
|
||||||
|
|
||||||
// mToolbar.inflateMenu(R.menu.main);
|
|
||||||
|
|
||||||
mDrawerLayout = findViewById<View>(R.id.drawer_layout) as DrawerLayout
|
|
||||||
if (mDrawerLayout != null) {
|
|
||||||
val navigationView = findViewById<View>(R.id.nav_view) as NavigationView
|
|
||||||
?: throw IllegalStateException("Layout requires a NavigationView " + "with id 'nav_view'")
|
|
||||||
|
|
||||||
// Create an ActionBarDrawerToggle that will handle opening/closing of the drawer:
|
|
||||||
mDrawerToggle = ActionBarDrawerToggle(this, mDrawerLayout,
|
|
||||||
mToolbar, R.string.navigation_drawer_open, R.string.navigation_drawer_close)
|
|
||||||
|
|
||||||
mDrawerLayout!!.addDrawerListener(drawerListener)
|
|
||||||
|
|
||||||
populateDrawerItems(navigationView)
|
|
||||||
setSupportActionBar(mToolbar)
|
|
||||||
updateDrawerToggle()
|
|
||||||
} else {
|
|
||||||
setSupportActionBar(mToolbar)
|
|
||||||
}
|
|
||||||
|
|
||||||
mToolbarInitialized = true
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun populateDrawerItems(navigationView: NavigationView) {
|
|
||||||
navigationView.setNavigationItemSelectedListener { menuItem ->
|
|
||||||
menuItem.isChecked = true
|
|
||||||
mItemToOpenWhenDrawerCloses = menuItem.itemId
|
|
||||||
mDrawerLayout!!.closeDrawers()
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
if (MainActivity::class.java.isAssignableFrom(javaClass)) {
|
|
||||||
navigationView.setCheckedItem(R.id.server_profile)
|
|
||||||
} else if (SubSettingActivity::class.java.isAssignableFrom(javaClass)) {
|
|
||||||
navigationView.setCheckedItem(R.id.sub_setting)
|
|
||||||
} else if (SettingsActivity::class.java.isAssignableFrom(javaClass)) {
|
|
||||||
navigationView.setCheckedItem(R.id.settings)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,21 +1,17 @@
|
|||||||
package com.v2ray.ang.ui
|
package com.v2ray.ang.ui
|
||||||
|
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.fragment.app.FragmentActivity
|
||||||
|
import androidx.viewpager2.adapter.FragmentStateAdapter
|
||||||
|
|
||||||
import android.support.v4.app.Fragment
|
class FragmentAdapter(fragmentActivity: FragmentActivity, private val mFragments: List<Fragment>) :
|
||||||
import android.support.v4.app.FragmentManager
|
FragmentStateAdapter(fragmentActivity) {
|
||||||
import android.support.v4.app.FragmentStatePagerAdapter
|
|
||||||
|
|
||||||
class FragmentAdapter(fm: FragmentManager, private val mFragments: List<Fragment>, private val mTitles: List<String>) : FragmentStatePagerAdapter(fm) {
|
override fun createFragment(position: Int): Fragment {
|
||||||
|
|
||||||
override fun getItem(position: Int): Fragment {
|
|
||||||
return mFragments[position]
|
return mFragments[position]
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getCount(): Int {
|
override fun getItemCount(): Int {
|
||||||
return mFragments.size
|
return mFragments.size
|
||||||
}
|
}
|
||||||
|
}
|
||||||
override fun getPageTitle(position: Int): CharSequence? {
|
|
||||||
return mTitles[position]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,27 +1,33 @@
|
|||||||
package com.v2ray.ang.ui
|
package com.v2ray.ang.ui
|
||||||
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
|
import android.os.Bundle
|
||||||
import android.text.method.ScrollingMovementMethod
|
import android.text.method.ScrollingMovementMethod
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import com.v2ray.ang.AppConfig.ANG_PACKAGE
|
||||||
import com.v2ray.ang.R
|
import com.v2ray.ang.R
|
||||||
|
import com.v2ray.ang.databinding.ActivityLogcatBinding
|
||||||
|
import com.v2ray.ang.extension.toast
|
||||||
import com.v2ray.ang.util.Utils
|
import com.v2ray.ang.util.Utils
|
||||||
import kotlinx.android.synthetic.main.activity_logcat.*
|
import kotlinx.coroutines.Dispatchers
|
||||||
import org.jetbrains.anko.doAsync
|
import kotlinx.coroutines.GlobalScope
|
||||||
import org.jetbrains.anko.toast
|
import kotlinx.coroutines.launch
|
||||||
import org.jetbrains.anko.uiThread
|
|
||||||
|
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.util.LinkedHashSet
|
import java.util.LinkedHashSet
|
||||||
|
|
||||||
class LogcatActivity : BaseActivity() {
|
class LogcatActivity : BaseActivity() {
|
||||||
|
private lateinit var binding: ActivityLogcatBinding
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
setContentView(R.layout.activity_logcat)
|
binding = ActivityLogcatBinding.inflate(layoutInflater)
|
||||||
|
val view = binding.root
|
||||||
|
setContentView(view)
|
||||||
|
|
||||||
title = getString(R.string.title_logcat)
|
title = getString(R.string.title_logcat)
|
||||||
|
|
||||||
@@ -32,9 +38,9 @@ class LogcatActivity : BaseActivity() {
|
|||||||
private fun logcat(shouldFlushLog: Boolean) {
|
private fun logcat(shouldFlushLog: Boolean) {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
pb_waiting.visibility = View.VISIBLE
|
binding.pbWaiting.visibility = View.VISIBLE
|
||||||
|
|
||||||
doAsync {
|
lifecycleScope.launch(Dispatchers.Default) {
|
||||||
if (shouldFlushLog) {
|
if (shouldFlushLog) {
|
||||||
val lst = LinkedHashSet<String>()
|
val lst = LinkedHashSet<String>()
|
||||||
lst.add("logcat")
|
lst.add("logcat")
|
||||||
@@ -48,17 +54,17 @@ class LogcatActivity : BaseActivity() {
|
|||||||
lst.add("-v")
|
lst.add("-v")
|
||||||
lst.add("time")
|
lst.add("time")
|
||||||
lst.add("-s")
|
lst.add("-s")
|
||||||
lst.add("GoLog,tun2socks,com.v2ray.ang")
|
lst.add("GoLog,tun2socks,${ANG_PACKAGE},AndroidRuntime,System.err")
|
||||||
val process = Runtime.getRuntime().exec(lst.toTypedArray())
|
val process = Runtime.getRuntime().exec(lst.toTypedArray())
|
||||||
// val bufferedReader = BufferedReader(
|
// val bufferedReader = BufferedReader(
|
||||||
// InputStreamReader(process.inputStream))
|
// InputStreamReader(process.inputStream))
|
||||||
// val allText = bufferedReader.use(BufferedReader::readText)
|
// val allText = bufferedReader.use(BufferedReader::readText)
|
||||||
val allText = process.inputStream.bufferedReader().use { it.readText() }
|
val allText = process.inputStream.bufferedReader().use { it.readText() }
|
||||||
uiThread {
|
launch(Dispatchers.Main) {
|
||||||
tv_logcat.text = allText
|
binding.tvLogcat.text = allText
|
||||||
tv_logcat.movementMethod = ScrollingMovementMethod()
|
binding.tvLogcat.movementMethod = ScrollingMovementMethod()
|
||||||
pb_waiting.visibility = View.GONE
|
binding.pbWaiting.visibility = View.GONE
|
||||||
Handler(Looper.getMainLooper()).post { sv_logcat.fullScroll(View.FOCUS_DOWN) }
|
Handler(Looper.getMainLooper()).post { binding.svLogcat.fullScroll(View.FOCUS_DOWN) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
@@ -66,18 +72,18 @@ class LogcatActivity : BaseActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
|
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||||
menuInflater.inflate(R.menu.menu_logcat, menu)
|
menuInflater.inflate(R.menu.menu_logcat, menu)
|
||||||
return super.onCreateOptionsMenu(menu)
|
return super.onCreateOptionsMenu(menu)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
|
override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
|
||||||
R.id.copy_all -> {
|
R.id.copy_all -> {
|
||||||
Utils.setClipboard(this, tv_logcat.text.toString())
|
Utils.setClipboard(this, binding.tvLogcat.text.toString())
|
||||||
toast(R.string.toast_success)
|
toast(R.string.toast_success)
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
R.id.delete -> {
|
R.id.clear_all -> {
|
||||||
logcat(true)
|
logcat(true)
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,184 +4,213 @@ import android.Manifest
|
|||||||
import android.content.*
|
import android.content.*
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.net.VpnService
|
import android.net.VpnService
|
||||||
import android.support.v7.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import com.tbruyelle.rxpermissions.RxPermissions
|
import com.tbruyelle.rxpermissions.RxPermissions
|
||||||
import com.v2ray.ang.R
|
import com.v2ray.ang.R
|
||||||
import com.v2ray.ang.util.AngConfigManager
|
|
||||||
import com.v2ray.ang.util.Utils
|
|
||||||
import kotlinx.android.synthetic.main.activity_main.*
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.text.TextUtils
|
import android.text.TextUtils
|
||||||
import android.view.KeyEvent
|
import android.view.KeyEvent
|
||||||
import com.v2ray.ang.AppConfig
|
import com.v2ray.ang.AppConfig
|
||||||
import com.v2ray.ang.util.MessageUtil
|
import android.content.res.ColorStateList
|
||||||
import com.v2ray.ang.util.V2rayConfigUtil
|
import com.google.android.material.navigation.NavigationView
|
||||||
import org.jetbrains.anko.*
|
import androidx.core.content.ContextCompat
|
||||||
import java.lang.ref.SoftReference
|
import androidx.core.view.GravityCompat
|
||||||
import java.net.URL
|
import androidx.appcompat.app.ActionBarDrawerToggle
|
||||||
import android.content.IntentFilter
|
import androidx.recyclerview.widget.ItemTouchHelper
|
||||||
import android.support.design.widget.NavigationView
|
|
||||||
import android.support.v4.view.GravityCompat
|
|
||||||
import android.support.v7.app.ActionBarDrawerToggle
|
|
||||||
import android.support.v7.widget.helper.ItemTouchHelper
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
//import com.v2ray.ang.InappBuyActivity
|
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.Observable
|
||||||
import rx.android.schedulers.AndroidSchedulers
|
import rx.android.schedulers.AndroidSchedulers
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
import com.v2ray.ang.helper.SimpleItemTouchHelperCallback
|
import com.v2ray.ang.helper.SimpleItemTouchHelperCallback
|
||||||
import com.v2ray.ang.util.AngConfigManager.configs
|
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 {
|
class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedListener {
|
||||||
companion object {
|
private lateinit var binding: ActivityMainBinding
|
||||||
private const val REQUEST_CODE_VPN_PREPARE = 0
|
|
||||||
private const val REQUEST_SCAN = 1
|
|
||||||
private const val REQUEST_FILE_CHOOSER = 2
|
|
||||||
private const val REQUEST_SCAN_URL = 3
|
|
||||||
}
|
|
||||||
|
|
||||||
var isRunning = false
|
|
||||||
set(value) {
|
|
||||||
field = value
|
|
||||||
adapter.changeable = !value
|
|
||||||
if (value) {
|
|
||||||
fab.imageResource = R.drawable.ic_v
|
|
||||||
tv_test_state.text = getString(R.string.connection_connected)
|
|
||||||
} else {
|
|
||||||
fab.imageResource = R.drawable.ic_v_idle
|
|
||||||
tv_test_state.text = getString(R.string.connection_not_connected)
|
|
||||||
}
|
|
||||||
hideCircle()
|
|
||||||
}
|
|
||||||
|
|
||||||
private val adapter by lazy { MainRecyclerAdapter(this) }
|
private val adapter by lazy { MainRecyclerAdapter(this) }
|
||||||
|
private val mainStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_MAIN, MMKV.MULTI_PROCESS_MODE) }
|
||||||
|
private val settingsStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_SETTING, MMKV.MULTI_PROCESS_MODE) }
|
||||||
|
private val requestVpnPermission = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||||
|
if (it.resultCode == RESULT_OK) {
|
||||||
|
startV2Ray()
|
||||||
|
}
|
||||||
|
}
|
||||||
private var mItemTouchHelper: ItemTouchHelper? = null
|
private var mItemTouchHelper: ItemTouchHelper? = null
|
||||||
|
val mainViewModel: MainViewModel by viewModels()
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
setContentView(R.layout.activity_main)
|
binding = ActivityMainBinding.inflate(layoutInflater)
|
||||||
|
val view = binding.root
|
||||||
|
setContentView(view)
|
||||||
title = getString(R.string.title_server)
|
title = getString(R.string.title_server)
|
||||||
setSupportActionBar(toolbar)
|
setSupportActionBar(binding.toolbar)
|
||||||
|
|
||||||
fab.setOnClickListener {
|
binding.fab.setOnClickListener {
|
||||||
if (isRunning) {
|
if (mainViewModel.isRunning.value == true) {
|
||||||
Utils.stopVService(this)
|
Utils.stopVService(this)
|
||||||
} else {
|
} else if (settingsStorage?.decodeString(AppConfig.PREF_MODE) ?: "VPN" == "VPN") {
|
||||||
val intent = VpnService.prepare(this)
|
val intent = VpnService.prepare(this)
|
||||||
if (intent == null) {
|
if (intent == null) {
|
||||||
startV2Ray()
|
startV2Ray()
|
||||||
} else {
|
} else {
|
||||||
startActivityForResult(intent, REQUEST_CODE_VPN_PREPARE)
|
requestVpnPermission.launch(intent)
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
startV2Ray()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
layout_test.setOnClickListener {
|
binding.layoutTest.setOnClickListener {
|
||||||
if (isRunning) {
|
if (mainViewModel.isRunning.value == true) {
|
||||||
val socksPort = 10808//Utils.parseInt(defaultDPreference.getPrefString(SettingsActivity.PREF_SOCKS_PORT, "10808"))
|
setTestState(getString(R.string.connection_test_testing))
|
||||||
|
mainViewModel.testCurrentServerRealPing()
|
||||||
tv_test_state.text = getString(R.string.connection_test_testing)
|
|
||||||
doAsync {
|
|
||||||
val result = Utils.testConnection(this@MainActivity, socksPort)
|
|
||||||
uiThread {
|
|
||||||
tv_test_state.text = Utils.getEditable(result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// tv_test_state.text = getString(R.string.connection_test_fail)
|
// tv_test_state.text = getString(R.string.connection_test_fail)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
recycler_view.setHasFixedSize(true)
|
binding.recyclerView.setHasFixedSize(true)
|
||||||
recycler_view.layoutManager = LinearLayoutManager(this)
|
binding.recyclerView.layoutManager = LinearLayoutManager(this)
|
||||||
recycler_view.adapter = adapter
|
binding.recyclerView.adapter = adapter
|
||||||
|
|
||||||
val callback = SimpleItemTouchHelperCallback(adapter)
|
val callback = SimpleItemTouchHelperCallback(adapter)
|
||||||
mItemTouchHelper = ItemTouchHelper(callback)
|
mItemTouchHelper = ItemTouchHelper(callback)
|
||||||
mItemTouchHelper?.attachToRecyclerView(recycler_view)
|
mItemTouchHelper?.attachToRecyclerView(binding.recyclerView)
|
||||||
|
|
||||||
|
|
||||||
val toggle = ActionBarDrawerToggle(
|
val toggle = ActionBarDrawerToggle(
|
||||||
this, drawer_layout, toolbar, R.string.navigation_drawer_open, R.string.navigation_drawer_close)
|
this, binding.drawerLayout, binding.toolbar, R.string.navigation_drawer_open, R.string.navigation_drawer_close)
|
||||||
drawer_layout.addDrawerListener(toggle)
|
binding.drawerLayout.addDrawerListener(toggle)
|
||||||
toggle.syncState()
|
toggle.syncState()
|
||||||
nav_view.setNavigationItemSelectedListener(this)
|
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() {
|
fun startV2Ray() {
|
||||||
if (AngConfigManager.configs.index < 0) {
|
if (mainStorage?.decodeString(MmkvManager.KEY_SELECTED_SERVER).isNullOrEmpty()) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
showCircle()
|
showCircle()
|
||||||
// toast(R.string.toast_services_start)
|
// toast(R.string.toast_services_start)
|
||||||
if (!Utils.startVService(this)) {
|
V2RayServiceManager.startV2Ray(this)
|
||||||
hideCircle()
|
hideCircle()
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStart() {
|
fun restartV2Ray() {
|
||||||
super.onStart()
|
if (mainViewModel.isRunning.value == true) {
|
||||||
isRunning = false
|
Utils.stopVService(this)
|
||||||
|
|
||||||
// val intent = Intent(this.applicationContext, V2RayVpnService::class.java)
|
|
||||||
// intent.`package` = AppConfig.ANG_PACKAGE
|
|
||||||
// bindService(intent, mConnection, BIND_AUTO_CREATE)
|
|
||||||
|
|
||||||
mMsgReceive = ReceiveMessageHandler(this@MainActivity)
|
|
||||||
registerReceiver(mMsgReceive, IntentFilter(AppConfig.BROADCAST_ACTION_ACTIVITY))
|
|
||||||
MessageUtil.sendMsg2Service(this, AppConfig.MSG_REGISTER_CLIENT, "")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onStop() {
|
|
||||||
super.onStop()
|
|
||||||
if (mMsgReceive != null) {
|
|
||||||
unregisterReceiver(mMsgReceive)
|
|
||||||
mMsgReceive = null
|
|
||||||
}
|
}
|
||||||
|
Observable.timer(500, TimeUnit.MILLISECONDS)
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe {
|
||||||
|
startV2Ray()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public override fun onResume() {
|
public override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
adapter.updateConfigList()
|
mainViewModel.reloadServerList()
|
||||||
}
|
}
|
||||||
|
|
||||||
public override fun onPause() {
|
public override fun onPause() {
|
||||||
super.onPause()
|
super.onPause()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||||
super.onActivityResult(requestCode, resultCode, data)
|
|
||||||
when (requestCode) {
|
|
||||||
REQUEST_CODE_VPN_PREPARE ->
|
|
||||||
if (resultCode == RESULT_OK) {
|
|
||||||
startV2Ray()
|
|
||||||
}
|
|
||||||
REQUEST_SCAN ->
|
|
||||||
if (resultCode == RESULT_OK) {
|
|
||||||
importBatchConfig(data?.getStringExtra("SCAN_RESULT"))
|
|
||||||
}
|
|
||||||
REQUEST_FILE_CHOOSER -> {
|
|
||||||
if (resultCode == RESULT_OK) {
|
|
||||||
val uri = data!!.data
|
|
||||||
readContentFromUri(uri)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
REQUEST_SCAN_URL ->
|
|
||||||
if (resultCode == RESULT_OK) {
|
|
||||||
importConfigCustomUrl(data?.getStringExtra("SCAN_RESULT"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
|
|
||||||
menuInflater.inflate(R.menu.menu_main, menu)
|
menuInflater.inflate(R.menu.menu_main, menu)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
|
override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
|
||||||
R.id.import_qrcode -> {
|
R.id.import_qrcode -> {
|
||||||
importQRcode(REQUEST_SCAN)
|
importQRcode(true)
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
R.id.import_clipboard -> {
|
R.id.import_clipboard -> {
|
||||||
@@ -189,18 +218,23 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
|||||||
true
|
true
|
||||||
}
|
}
|
||||||
R.id.import_manually_vmess -> {
|
R.id.import_manually_vmess -> {
|
||||||
startActivity<ServerActivity>("position" to -1, "isRunning" to isRunning)
|
importManually(EConfigType.VMESS.value)
|
||||||
adapter.updateConfigList()
|
true
|
||||||
|
}
|
||||||
|
R.id.import_manually_vless -> {
|
||||||
|
importManually(EConfigType.VLESS.value)
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
R.id.import_manually_ss -> {
|
R.id.import_manually_ss -> {
|
||||||
startActivity<Server3Activity>("position" to -1, "isRunning" to isRunning)
|
importManually(EConfigType.SHADOWSOCKS.value)
|
||||||
adapter.updateConfigList()
|
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
R.id.import_manually_socks -> {
|
R.id.import_manually_socks -> {
|
||||||
startActivity<Server4Activity>("position" to -1, "isRunning" to isRunning)
|
importManually(EConfigType.SOCKS.value)
|
||||||
adapter.updateConfigList()
|
true
|
||||||
|
}
|
||||||
|
R.id.import_manually_trojan -> {
|
||||||
|
importManually(EConfigType.TROJAN.value)
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
R.id.import_config_custom_clipboard -> {
|
R.id.import_config_custom_clipboard -> {
|
||||||
@@ -216,7 +250,7 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
|||||||
true
|
true
|
||||||
}
|
}
|
||||||
R.id.import_config_custom_url_scan -> {
|
R.id.import_config_custom_url_scan -> {
|
||||||
importQRcode(REQUEST_SCAN_URL)
|
importQRcode(false)
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -231,8 +265,8 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
|||||||
}
|
}
|
||||||
|
|
||||||
R.id.export_all -> {
|
R.id.export_all -> {
|
||||||
if (AngConfigManager.shareAll2Clipboard() == 0) {
|
if (AngConfigManager.shareNonCustomConfigsToClipboard(this, mainViewModel.serverList) == 0) {
|
||||||
//remove toast, otherwise it will block previous warning message
|
toast(R.string.toast_success)
|
||||||
} else {
|
} else {
|
||||||
toast(R.string.toast_failure)
|
toast(R.string.toast_failure)
|
||||||
}
|
}
|
||||||
@@ -240,39 +274,65 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
|||||||
}
|
}
|
||||||
|
|
||||||
R.id.ping_all -> {
|
R.id.ping_all -> {
|
||||||
for (k in 0 until configs.vmess.count()) {
|
mainViewModel.testAllTcping()
|
||||||
configs.vmess[k].testResult = ""
|
true
|
||||||
adapter.updateConfigList()
|
}
|
||||||
}
|
|
||||||
for (k in 0 until configs.vmess.count()) {
|
R.id.real_ping_all -> {
|
||||||
if (configs.vmess[k].configType != AppConfig.EConfigType.Custom) {
|
mainViewModel.testAllRealPing()
|
||||||
doAsync {
|
true
|
||||||
configs.vmess[k].testResult = Utils.tcping(configs.vmess[k].address, configs.vmess[k].port)
|
}
|
||||||
uiThread {
|
|
||||||
adapter.updateSelectedItem(k)
|
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
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
// R.id.settings -> {
|
|
||||||
// startActivity<SettingsActivity>("isRunning" to isRunning)
|
|
||||||
// true
|
|
||||||
// }
|
|
||||||
// R.id.logcat -> {
|
|
||||||
// startActivity<LogcatActivity>()
|
|
||||||
// true
|
|
||||||
// }
|
|
||||||
else -> super.onOptionsItemSelected(item)
|
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
|
* import config from qrcode
|
||||||
*/
|
*/
|
||||||
fun importQRcode(requestCode: Int): Boolean {
|
fun importQRcode(forConfig: Boolean): Boolean {
|
||||||
// try {
|
// try {
|
||||||
// startActivityForResult(Intent("com.google.zxing.client.android.SCAN")
|
// startActivityForResult(Intent("com.google.zxing.client.android.SCAN")
|
||||||
// .addCategory(Intent.CATEGORY_DEFAULT)
|
// .addCategory(Intent.CATEGORY_DEFAULT)
|
||||||
@@ -282,7 +342,10 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
|||||||
.request(Manifest.permission.CAMERA)
|
.request(Manifest.permission.CAMERA)
|
||||||
.subscribe {
|
.subscribe {
|
||||||
if (it)
|
if (it)
|
||||||
startActivityForResult<ScannerActivity>(requestCode)
|
if (forConfig)
|
||||||
|
scanQRCodeForConfig.launch(Intent(this, ScannerActivity::class.java))
|
||||||
|
else
|
||||||
|
scanQRCodeForUrlToCustomConfig.launch(Intent(this, ScannerActivity::class.java))
|
||||||
else
|
else
|
||||||
toast(R.string.toast_permission_denied)
|
toast(R.string.toast_permission_denied)
|
||||||
}
|
}
|
||||||
@@ -290,6 +353,18 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
|||||||
return true
|
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
|
* import config from clipboard
|
||||||
*/
|
*/
|
||||||
@@ -306,10 +381,20 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun importBatchConfig(server: String?, subid: String = "") {
|
fun importBatchConfig(server: String?, subid: String = "") {
|
||||||
val count = AngConfigManager.importBatchConfig(server, subid)
|
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) {
|
if (count > 0) {
|
||||||
toast(R.string.toast_success)
|
toast(R.string.toast_success)
|
||||||
adapter.updateConfigList()
|
mainViewModel.reloadServerList()
|
||||||
} else {
|
} else {
|
||||||
toast(R.string.toast_failure)
|
toast(R.string.toast_failure)
|
||||||
}
|
}
|
||||||
@@ -368,9 +453,14 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
|||||||
toast(R.string.toast_invalid_url)
|
toast(R.string.toast_invalid_url)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
doAsync {
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
val configText = URL(url).readText()
|
val configText = try {
|
||||||
uiThread {
|
Utils.getUrlContentWithCustomUserAgent(url)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
""
|
||||||
|
}
|
||||||
|
launch(Dispatchers.Main) {
|
||||||
importCustomizeConfig(configText)
|
importCustomizeConfig(configText)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -388,24 +478,33 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
|||||||
: Boolean {
|
: Boolean {
|
||||||
try {
|
try {
|
||||||
toast(R.string.title_sub_update)
|
toast(R.string.title_sub_update)
|
||||||
val subItem = AngConfigManager.configs.subItem
|
MmkvManager.decodeSubscriptions().forEach {
|
||||||
for (k in 0 until subItem.count()) {
|
if (TextUtils.isEmpty(it.first)
|
||||||
if (TextUtils.isEmpty(subItem[k].id)
|
|| TextUtils.isEmpty(it.second.remarks)
|
||||||
|| TextUtils.isEmpty(subItem[k].remarks)
|
|| TextUtils.isEmpty(it.second.url)
|
||||||
|| TextUtils.isEmpty(subItem[k].url)
|
|
||||||
) {
|
) {
|
||||||
continue
|
return@forEach
|
||||||
}
|
}
|
||||||
val id = subItem[k].id
|
if (!it.second.enabled) {
|
||||||
val url = subItem[k].url
|
return@forEach
|
||||||
|
}
|
||||||
|
val url = it.second.url
|
||||||
if (!Utils.isValidUrl(url)) {
|
if (!Utils.isValidUrl(url)) {
|
||||||
continue
|
return@forEach
|
||||||
}
|
}
|
||||||
Log.d("Main", url)
|
Log.d(ANG_PACKAGE, url)
|
||||||
doAsync {
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
val configText = URL(url).readText()
|
val configText = try {
|
||||||
uiThread {
|
Utils.getUrlContentWithCustomUserAgent(url)
|
||||||
importBatchConfig(Utils.decode(configText), id)
|
} 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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -425,14 +524,19 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
|||||||
intent.addCategory(Intent.CATEGORY_OPENABLE)
|
intent.addCategory(Intent.CATEGORY_OPENABLE)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
startActivityForResult(
|
chooseFileForCustomConfig.launch(Intent.createChooser(intent, getString(R.string.title_file_chooser)))
|
||||||
Intent.createChooser(intent, getString(R.string.title_file_chooser)),
|
} catch (ex: ActivityNotFoundException) {
|
||||||
REQUEST_FILE_CHOOSER)
|
|
||||||
} catch (ex: android.content.ActivityNotFoundException) {
|
|
||||||
toast(R.string.toast_require_file_manager)
|
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
|
* read content from uri
|
||||||
*/
|
*/
|
||||||
@@ -442,9 +546,9 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
|||||||
.subscribe {
|
.subscribe {
|
||||||
if (it) {
|
if (it) {
|
||||||
try {
|
try {
|
||||||
val inputStream = contentResolver.openInputStream(uri)
|
contentResolver.openInputStream(uri).use { input ->
|
||||||
val configText = inputStream.bufferedReader().readText()
|
importCustomizeConfig(input?.bufferedReader()?.readText())
|
||||||
importCustomizeConfig(configText)
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
}
|
}
|
||||||
@@ -457,22 +561,26 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
|||||||
* import customize config
|
* import customize config
|
||||||
*/
|
*/
|
||||||
fun importCustomizeConfig(server: String?) {
|
fun importCustomizeConfig(server: String?) {
|
||||||
if (server == null) {
|
try {
|
||||||
return
|
if (server == null || TextUtils.isEmpty(server)) {
|
||||||
}
|
toast(R.string.toast_none_data)
|
||||||
if (!V2rayConfigUtil.isValidConfig(server)) {
|
return
|
||||||
toast(R.string.toast_config_file_invalid)
|
}
|
||||||
return
|
mainViewModel.appendCustomConfigServer(server)
|
||||||
}
|
mainViewModel.reloadServerList()
|
||||||
val resId = AngConfigManager.importCustomizeConfig(server)
|
|
||||||
if (resId > 0) {
|
|
||||||
toast(resId)
|
|
||||||
} else {
|
|
||||||
toast(R.string.toast_success)
|
toast(R.string.toast_success)
|
||||||
adapter.updateConfigList()
|
//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 {
|
// val mConnection = object : ServiceConnection {
|
||||||
// override fun onServiceDisconnected(name: ComponentName?) {
|
// override fun onServiceDisconnected(name: ComponentName?) {
|
||||||
// }
|
// }
|
||||||
@@ -482,35 +590,6 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
|||||||
// }
|
// }
|
||||||
// }
|
// }
|
||||||
|
|
||||||
private
|
|
||||||
var mMsgReceive: BroadcastReceiver? = null
|
|
||||||
|
|
||||||
private class ReceiveMessageHandler(activity: MainActivity) : BroadcastReceiver() {
|
|
||||||
internal var mReference: SoftReference<MainActivity> = SoftReference(activity)
|
|
||||||
override fun onReceive(ctx: Context?, intent: Intent?) {
|
|
||||||
val activity = mReference.get()
|
|
||||||
when (intent?.getIntExtra("key", 0)) {
|
|
||||||
AppConfig.MSG_STATE_RUNNING -> {
|
|
||||||
activity?.isRunning = true
|
|
||||||
}
|
|
||||||
AppConfig.MSG_STATE_NOT_RUNNING -> {
|
|
||||||
activity?.isRunning = false
|
|
||||||
}
|
|
||||||
AppConfig.MSG_STATE_START_SUCCESS -> {
|
|
||||||
activity?.toast(R.string.toast_services_success)
|
|
||||||
activity?.isRunning = true
|
|
||||||
}
|
|
||||||
AppConfig.MSG_STATE_START_FAILURE -> {
|
|
||||||
activity?.toast(R.string.toast_services_failure)
|
|
||||||
activity?.isRunning = false
|
|
||||||
}
|
|
||||||
AppConfig.MSG_STATE_STOP_SUCCESS -> {
|
|
||||||
activity?.isRunning = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean {
|
override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean {
|
||||||
if (keyCode == KeyEvent.KEYCODE_BACK) {
|
if (keyCode == KeyEvent.KEYCODE_BACK) {
|
||||||
moveTaskToBack(false)
|
moveTaskToBack(false)
|
||||||
@@ -520,7 +599,7 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun showCircle() {
|
fun showCircle() {
|
||||||
fabProgressCircle?.show()
|
binding.fabProgressCircle.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun hideCircle() {
|
fun hideCircle() {
|
||||||
@@ -528,17 +607,22 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
|||||||
Observable.timer(300, TimeUnit.MILLISECONDS)
|
Observable.timer(300, TimeUnit.MILLISECONDS)
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribe {
|
.subscribe {
|
||||||
if (fabProgressCircle.isShown) {
|
try {
|
||||||
fabProgressCircle.hide()
|
if (binding.fabProgressCircle.isShown) {
|
||||||
|
binding.fabProgressCircle.hide()
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.w(ANG_PACKAGE, e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
Log.d(ANG_PACKAGE, e.toString())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBackPressed() {
|
override fun onBackPressed() {
|
||||||
if (drawer_layout.isDrawerOpen(GravityCompat.START)) {
|
if (binding.drawerLayout.isDrawerOpen(GravityCompat.START)) {
|
||||||
drawer_layout.closeDrawer(GravityCompat.START)
|
binding.drawerLayout.closeDrawer(GravityCompat.START)
|
||||||
} else {
|
} else {
|
||||||
super.onBackPressed()
|
super.onBackPressed()
|
||||||
}
|
}
|
||||||
@@ -549,25 +633,26 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
|||||||
when (item.itemId) {
|
when (item.itemId) {
|
||||||
//R.id.server_profile -> activityClass = MainActivity::class.java
|
//R.id.server_profile -> activityClass = MainActivity::class.java
|
||||||
R.id.sub_setting -> {
|
R.id.sub_setting -> {
|
||||||
startActivity<SubSettingActivity>()
|
startActivity(Intent(this, SubSettingActivity::class.java))
|
||||||
}
|
}
|
||||||
R.id.settings -> {
|
R.id.settings -> {
|
||||||
startActivity<SettingsActivity>("isRunning" to isRunning)
|
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 -> {
|
R.id.feedback -> {
|
||||||
Utils.openUri(this, AppConfig.v2rayNGIssues)
|
Utils.openUri(this, AppConfig.v2rayNGIssues)
|
||||||
}
|
}
|
||||||
R.id.promotion -> {
|
R.id.promotion -> {
|
||||||
Utils.openUri(this, AppConfig.promotionUrl)
|
Utils.openUri(this, "${Utils.decode(AppConfig.promotionUrl)}?t=${System.currentTimeMillis()}")
|
||||||
}
|
|
||||||
R.id.donate -> {
|
|
||||||
// startActivity<InappBuyActivity>()
|
|
||||||
}
|
}
|
||||||
R.id.logcat -> {
|
R.id.logcat -> {
|
||||||
startActivity<LogcatActivity>()
|
startActivity(Intent(this, LogcatActivity::class.java))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
drawer_layout.closeDrawer(GravityCompat.START)
|
binding.drawerLayout.closeDrawer(GravityCompat.START)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,24 +1,33 @@
|
|||||||
package com.v2ray.ang.ui
|
package com.v2ray.ang.ui
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import android.support.v7.widget.RecyclerView
|
|
||||||
import android.text.TextUtils
|
import android.text.TextUtils
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
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.AppConfig
|
||||||
import com.v2ray.ang.R
|
import com.v2ray.ang.R
|
||||||
import com.v2ray.ang.dto.AngConfig
|
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.ItemTouchHelperAdapter
|
||||||
import com.v2ray.ang.helper.ItemTouchHelperViewHolder
|
import com.v2ray.ang.helper.ItemTouchHelperViewHolder
|
||||||
|
import com.v2ray.ang.service.V2RayServiceManager
|
||||||
import com.v2ray.ang.util.AngConfigManager
|
import com.v2ray.ang.util.AngConfigManager
|
||||||
|
import com.v2ray.ang.util.MmkvManager
|
||||||
import com.v2ray.ang.util.Utils
|
import com.v2ray.ang.util.Utils
|
||||||
import kotlinx.android.synthetic.main.item_qrcode.view.*
|
|
||||||
import kotlinx.android.synthetic.main.item_recycler_main.view.*
|
|
||||||
import org.jetbrains.anko.*
|
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import rx.android.schedulers.AndroidSchedulers
|
import rx.android.schedulers.AndroidSchedulers
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
import com.v2ray.ang.extension.defaultDPreference
|
|
||||||
|
|
||||||
class MainRecyclerAdapter(val activity: MainActivity) : RecyclerView.Adapter<MainRecyclerAdapter.BaseViewHolder>()
|
class MainRecyclerAdapter(val activity: MainActivity) : RecyclerView.Adapter<MainRecyclerAdapter.BaseViewHolder>()
|
||||||
, ItemTouchHelperAdapter {
|
, ItemTouchHelperAdapter {
|
||||||
@@ -28,241 +37,224 @@ class MainRecyclerAdapter(val activity: MainActivity) : RecyclerView.Adapter<Mai
|
|||||||
}
|
}
|
||||||
|
|
||||||
private var mActivity: MainActivity = activity
|
private var mActivity: MainActivity = activity
|
||||||
private lateinit var configs: AngConfig
|
private val mainStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_MAIN, MMKV.MULTI_PROCESS_MODE) }
|
||||||
|
private val subStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_SUB, MMKV.MULTI_PROCESS_MODE) }
|
||||||
|
private val settingsStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_SETTING, MMKV.MULTI_PROCESS_MODE) }
|
||||||
private val share_method: Array<out String> by lazy {
|
private val share_method: Array<out String> by lazy {
|
||||||
mActivity.resources.getStringArray(R.array.share_method)
|
mActivity.resources.getStringArray(R.array.share_method)
|
||||||
}
|
}
|
||||||
|
var isRunning = false
|
||||||
|
|
||||||
var changeable: Boolean = true
|
override fun getItemCount() = mActivity.mainViewModel.serversCache.size + 1
|
||||||
set(value) {
|
|
||||||
if (field == value)
|
|
||||||
return
|
|
||||||
field = value
|
|
||||||
notifyDataSetChanged()
|
|
||||||
}
|
|
||||||
|
|
||||||
init {
|
|
||||||
updateConfigList()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getItemCount() = configs.vmess.count() + 1
|
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: BaseViewHolder, position: Int) {
|
override fun onBindViewHolder(holder: BaseViewHolder, position: Int) {
|
||||||
if (holder is MainViewHolder) {
|
if (holder is MainViewHolder) {
|
||||||
val configType = configs.vmess[position].configType
|
val guid = mActivity.mainViewModel.serversCache[position].guid
|
||||||
val remarks = configs.vmess[position].remarks
|
val config = mActivity.mainViewModel.serversCache[position].config
|
||||||
val subid = configs.vmess[position].subid
|
// //filter
|
||||||
val address = configs.vmess[position].address
|
// if (mActivity.mainViewModel.subscriptionId.isNotEmpty()
|
||||||
val port = configs.vmess[position].port
|
// && mActivity.mainViewModel.subscriptionId != config.subscriptionId
|
||||||
val test_result = configs.vmess[position].testResult
|
// ) {
|
||||||
|
// holder.itemMainBinding.cardView.visibility = View.GONE
|
||||||
|
// } else {
|
||||||
|
// holder.itemMainBinding.cardView.visibility = View.VISIBLE
|
||||||
|
// }
|
||||||
|
|
||||||
holder.name.text = remarks
|
val outbound = config.getProxyOutbound()
|
||||||
holder.radio.isChecked = (position == configs.index)
|
val aff = MmkvManager.decodeServerAffiliationInfo(guid)
|
||||||
holder.itemView.backgroundColor = Color.TRANSPARENT
|
|
||||||
holder.test_result.text = test_result
|
|
||||||
|
|
||||||
if (TextUtils.isEmpty(subid)) {
|
holder.itemMainBinding.tvName.text = config.remarks
|
||||||
holder.subid.text = ""
|
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 {
|
} else {
|
||||||
holder.subid.text = "S"
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
if (configType == AppConfig.EConfigType.Vmess) {
|
var shareOptions = share_method.asList()
|
||||||
holder.type.text = "vmess"
|
when (config.configType) {
|
||||||
holder.statistics.text = "$address : $port"
|
EConfigType.CUSTOM -> {
|
||||||
holder.layout_share.visibility = View.VISIBLE
|
holder.itemMainBinding.tvType.text = mActivity.getString(R.string.server_customize_config)
|
||||||
} else if (configType == AppConfig.EConfigType.Custom) {
|
shareOptions = shareOptions.takeLast(1)
|
||||||
holder.type.text = mActivity.getString(R.string.server_customize_config)
|
}
|
||||||
holder.statistics.text = ""//mActivity.getString(R.string.server_customize_config)
|
EConfigType.VLESS -> {
|
||||||
holder.layout_share.visibility = View.INVISIBLE
|
holder.itemMainBinding.tvType.text = config.configType.name
|
||||||
} else if (configType == AppConfig.EConfigType.Shadowsocks) {
|
}
|
||||||
holder.type.text = "shadowsocks"
|
else -> {
|
||||||
holder.statistics.text = "$address : $port"
|
holder.itemMainBinding.tvType.text = config.configType.name.lowercase()
|
||||||
holder.layout_share.visibility = View.VISIBLE
|
}
|
||||||
} else if (configType == AppConfig.EConfigType.Socks) {
|
|
||||||
holder.type.text = "socks"
|
|
||||||
holder.statistics.text = "$address : $port"
|
|
||||||
holder.layout_share.visibility = View.VISIBLE
|
|
||||||
}
|
}
|
||||||
|
holder.itemMainBinding.tvStatistics.text = "${outbound?.getServerAddress()} : ${outbound?.getServerPort()}"
|
||||||
|
|
||||||
holder.layout_share.setOnClickListener {
|
holder.itemMainBinding.layoutShare.setOnClickListener {
|
||||||
mActivity.selector(null, share_method.asList()) { dialogInterface, i ->
|
AlertDialog.Builder(mActivity).setItems(shareOptions.toTypedArray()) { _, i ->
|
||||||
try {
|
try {
|
||||||
when (i) {
|
when (i) {
|
||||||
0 -> {
|
0 -> {
|
||||||
val iv = mActivity.layoutInflater.inflate(R.layout.item_qrcode, null)
|
if (config.configType == EConfigType.CUSTOM) {
|
||||||
iv.iv_qcode.setImageBitmap(AngConfigManager.share2QRCode(position))
|
shareFullContent(guid)
|
||||||
|
} else {
|
||||||
mActivity.alert {
|
val ivBinding = ItemQrcodeBinding.inflate(LayoutInflater.from(mActivity))
|
||||||
customView {
|
ivBinding.ivQcode.setImageBitmap(AngConfigManager.share2QRCode(guid))
|
||||||
linearLayout {
|
AlertDialog.Builder(mActivity).setView(ivBinding.root).show()
|
||||||
addView(iv)
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
}.show()
|
|
||||||
}
|
}
|
||||||
1 -> {
|
1 -> {
|
||||||
if (AngConfigManager.share2Clipboard(position) == 0) {
|
if (AngConfigManager.share2Clipboard(mActivity, guid) == 0) {
|
||||||
mActivity.toast(R.string.toast_success)
|
mActivity.toast(R.string.toast_success)
|
||||||
} else {
|
} else {
|
||||||
mActivity.toast(R.string.toast_failure)
|
mActivity.toast(R.string.toast_failure)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
2 -> {
|
2 -> shareFullContent(guid)
|
||||||
if (AngConfigManager.shareFullContent2Clipboard(position) == 0) {
|
else -> mActivity.toast("else")
|
||||||
mActivity.toast(R.string.toast_success)
|
|
||||||
} else {
|
|
||||||
mActivity.toast(R.string.toast_failure)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else ->
|
|
||||||
mActivity.toast("else")
|
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
}
|
}
|
||||||
}
|
}.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
holder.layout_edit.setOnClickListener {
|
holder.itemMainBinding.layoutEdit.setOnClickListener {
|
||||||
if (configType == AppConfig.EConfigType.Vmess) {
|
val intent = Intent().putExtra("guid", guid)
|
||||||
mActivity.startActivity<ServerActivity>("position" to position, "isRunning" to !changeable)
|
.putExtra("isRunning", isRunning)
|
||||||
} else if (configType == AppConfig.EConfigType.Custom) {
|
if (config.configType == EConfigType.CUSTOM) {
|
||||||
mActivity.startActivity<Server2Activity>("position" to position, "isRunning" to !changeable)
|
mActivity.startActivity(intent.setClass(mActivity, ServerCustomConfigActivity::class.java))
|
||||||
} else if (configType == AppConfig.EConfigType.Shadowsocks) {
|
} else {
|
||||||
mActivity.startActivity<Server3Activity>("position" to position, "isRunning" to !changeable)
|
mActivity.startActivity(intent.setClass(mActivity, ServerActivity::class.java))
|
||||||
} else if (configType == AppConfig.EConfigType.Socks) {
|
|
||||||
mActivity.startActivity<Server4Activity>("position" to position, "isRunning" to !changeable)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
holder.layout_remove.setOnClickListener {
|
holder.itemMainBinding.layoutRemove.setOnClickListener {
|
||||||
if (configs.index != position) {
|
if (guid != mainStorage?.decodeString(MmkvManager.KEY_SELECTED_SERVER)) {
|
||||||
if (AngConfigManager.removeServer(position) == 0) {
|
if (settingsStorage?.decodeBool(AppConfig.PREF_CONFIRM_REMOVE) == true) {
|
||||||
notifyItemRemoved(position)
|
AlertDialog.Builder(mActivity).setMessage(R.string.del_config_comfirm)
|
||||||
updateSelectedItem(position)
|
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||||
|
removeServer(guid, position)
|
||||||
|
}
|
||||||
|
.show()
|
||||||
|
} else {
|
||||||
|
removeServer(guid, position)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
holder.infoContainer.setOnClickListener {
|
holder.itemMainBinding.infoContainer.setOnClickListener {
|
||||||
if (changeable) {
|
val selected = mainStorage?.decodeString(MmkvManager.KEY_SELECTED_SERVER)
|
||||||
AngConfigManager.setActiveServer(position)
|
if (guid != selected) {
|
||||||
} else {
|
mainStorage?.encode(MmkvManager.KEY_SELECTED_SERVER, guid)
|
||||||
mActivity.showCircle()
|
if (!TextUtils.isEmpty(selected)) {
|
||||||
Utils.stopVService(mActivity)
|
notifyItemChanged(mActivity.mainViewModel.getPosition(selected!!))
|
||||||
AngConfigManager.setActiveServer(position)
|
}
|
||||||
Observable.timer(500, TimeUnit.MILLISECONDS)
|
notifyItemChanged(mActivity.mainViewModel.getPosition(guid))
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
if (isRunning) {
|
||||||
.subscribe {
|
mActivity.showCircle()
|
||||||
mActivity.showCircle()
|
Utils.stopVService(mActivity)
|
||||||
if (!Utils.startVService(mActivity)) {
|
Observable.timer(500, TimeUnit.MILLISECONDS)
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe {
|
||||||
|
V2RayServiceManager.startV2Ray(mActivity)
|
||||||
mActivity.hideCircle()
|
mActivity.hideCircle()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
notifyDataSetChanged()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (holder is FooterViewHolder) {
|
if (holder is FooterViewHolder) {
|
||||||
//if (activity?.defaultDPreference?.getPrefBoolean(AppConfig.PREF_INAPP_BUY_IS_PREMIUM, false)) {
|
//if (activity?.defaultDPreference?.getPrefBoolean(AppConfig.PREF_INAPP_BUY_IS_PREMIUM, false)) {
|
||||||
if (true) {
|
if (true) {
|
||||||
holder.layout_edit.visibility = View.INVISIBLE
|
holder.itemFooterBinding.layoutEdit.visibility = View.INVISIBLE
|
||||||
} else {
|
} else {
|
||||||
holder.layout_edit.setOnClickListener {
|
holder.itemFooterBinding.layoutEdit.setOnClickListener {
|
||||||
Utils.openUri(mActivity, AppConfig.promotionUrl)
|
Utils.openUri(mActivity, "${Utils.decode(AppConfig.promotionUrl)}?t=${System.currentTimeMillis()}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseViewHolder {
|
private fun shareFullContent(guid: String) {
|
||||||
when (viewType) {
|
if (AngConfigManager.shareFullContent2Clipboard(mActivity, guid) == 0) {
|
||||||
VIEW_TYPE_ITEM ->
|
mActivity.toast(R.string.toast_success)
|
||||||
return MainViewHolder(parent.context.layoutInflater
|
} else {
|
||||||
.inflate(R.layout.item_recycler_main, parent, false))
|
mActivity.toast(R.string.toast_failure)
|
||||||
else ->
|
|
||||||
return FooterViewHolder(parent.context.layoutInflater
|
|
||||||
.inflate(R.layout.item_recycler_footer, parent, false))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateConfigList() {
|
private fun removeServer(guid: String,position:Int) {
|
||||||
configs = AngConfigManager.configs
|
mActivity.mainViewModel.removeServer(guid)
|
||||||
notifyDataSetChanged()
|
notifyItemRemoved(position)
|
||||||
|
notifyItemRangeChanged(position, mActivity.mainViewModel.serversCache.size)
|
||||||
}
|
}
|
||||||
|
|
||||||
// fun updateSelectedItem() {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseViewHolder {
|
||||||
// updateSelectedItem(configs.index)
|
return when (viewType) {
|
||||||
// }
|
VIEW_TYPE_ITEM ->
|
||||||
|
MainViewHolder(ItemRecyclerMainBinding.inflate(LayoutInflater.from(parent.context), parent, false))
|
||||||
fun updateSelectedItem(pos: Int) {
|
else ->
|
||||||
//notifyItemChanged(pos)
|
FooterViewHolder(ItemRecyclerFooterBinding.inflate(LayoutInflater.from(parent.context), parent, false))
|
||||||
notifyItemRangeChanged(pos, itemCount - pos)
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getItemViewType(position: Int): Int {
|
override fun getItemViewType(position: Int): Int {
|
||||||
if (position == configs.vmess.count()) {
|
return if (position == mActivity.mainViewModel.serversCache.size) {
|
||||||
return VIEW_TYPE_FOOTER
|
VIEW_TYPE_FOOTER
|
||||||
} else {
|
} else {
|
||||||
return VIEW_TYPE_ITEM
|
VIEW_TYPE_ITEM
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
open class BaseViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView)
|
open class BaseViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||||
|
fun onItemSelected() {
|
||||||
class MainViewHolder(itemView: View) : BaseViewHolder(itemView), ItemTouchHelperViewHolder {
|
|
||||||
val subid = itemView.tv_subid
|
|
||||||
val radio = itemView.btn_radio!!
|
|
||||||
val name = itemView.tv_name!!
|
|
||||||
val test_result = itemView.tv_test_result!!
|
|
||||||
val type = itemView.tv_type!!
|
|
||||||
val statistics = itemView.tv_statistics!!
|
|
||||||
val infoContainer = itemView.info_container!!
|
|
||||||
val layout_edit = itemView.layout_edit!!
|
|
||||||
val layout_share = itemView.layout_share
|
|
||||||
val layout_remove = itemView.layout_remove!!
|
|
||||||
|
|
||||||
override fun onItemSelected() {
|
|
||||||
itemView.setBackgroundColor(Color.LTGRAY)
|
itemView.setBackgroundColor(Color.LTGRAY)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onItemClear() {
|
fun onItemClear() {
|
||||||
itemView.setBackgroundColor(0)
|
itemView.setBackgroundColor(0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class FooterViewHolder(itemView: View) : BaseViewHolder(itemView), ItemTouchHelperViewHolder {
|
class MainViewHolder(val itemMainBinding: ItemRecyclerMainBinding) :
|
||||||
val layout_edit = itemView.layout_edit!!
|
BaseViewHolder(itemMainBinding.root), ItemTouchHelperViewHolder
|
||||||
|
|
||||||
override fun onItemSelected() {
|
class FooterViewHolder(val itemFooterBinding: ItemRecyclerFooterBinding) :
|
||||||
itemView.setBackgroundColor(Color.LTGRAY)
|
BaseViewHolder(itemFooterBinding.root), ItemTouchHelperViewHolder
|
||||||
}
|
|
||||||
|
|
||||||
override fun onItemClear() {
|
|
||||||
itemView.setBackgroundColor(0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onItemDismiss(position: Int) {
|
override fun onItemDismiss(position: Int) {
|
||||||
if (configs.index != position) {
|
val guid = mActivity.mainViewModel.serversCache.getOrNull(position)?.guid ?: return
|
||||||
|
if (guid != mainStorage?.decodeString(MmkvManager.KEY_SELECTED_SERVER)) {
|
||||||
// mActivity.alert(R.string.del_config_comfirm) {
|
// mActivity.alert(R.string.del_config_comfirm) {
|
||||||
// positiveButton(android.R.string.ok) {
|
// positiveButton(android.R.string.ok) {
|
||||||
if (AngConfigManager.removeServer(position) == 0) {
|
mActivity.mainViewModel.removeServer(guid)
|
||||||
notifyItemRemoved(position)
|
notifyItemRemoved(position)
|
||||||
}
|
|
||||||
// }
|
// }
|
||||||
// show()
|
// show()
|
||||||
// }
|
// }
|
||||||
}
|
}
|
||||||
updateSelectedItem(position)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onItemMove(fromPosition: Int, toPosition: Int): Boolean {
|
override fun onItemMove(fromPosition: Int, toPosition: Int): Boolean {
|
||||||
AngConfigManager.swapServer(fromPosition, toPosition)
|
mActivity.mainViewModel.swapServer(fromPosition, toPosition)
|
||||||
notifyItemMoved(fromPosition, toPosition)
|
notifyItemMoved(fromPosition, toPosition)
|
||||||
//notifyDataSetChanged()
|
// position is changed, since position is used by click callbacks, need to update range
|
||||||
updateSelectedItem(if (fromPosition < toPosition) fromPosition else toPosition)
|
if (toPosition > fromPosition)
|
||||||
|
notifyItemRangeChanged(fromPosition, toPosition - fromPosition + 1)
|
||||||
|
else
|
||||||
|
notifyItemRangeChanged(toPosition, fromPosition - toPosition + 1)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onItemMoveCompleted() {
|
||||||
|
// do nothing
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,57 +1,52 @@
|
|||||||
package com.v2ray.ang.ui
|
package com.v2ray.ang.ui
|
||||||
|
|
||||||
import android.animation.Animator
|
|
||||||
import android.animation.AnimatorListenerAdapter
|
|
||||||
import android.content.Context
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.support.v7.widget.RecyclerView
|
|
||||||
import android.text.TextUtils
|
import android.text.TextUtils
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.animation.AccelerateInterpolator
|
import androidx.appcompat.widget.SearchView
|
||||||
import android.view.animation.DecelerateInterpolator
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.dinuscxj.itemdecoration.LinearDividerItemDecoration
|
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.R
|
||||||
import com.v2ray.ang.extension.defaultDPreference
|
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.AppManagerUtil
|
||||||
import kotlinx.android.synthetic.main.activity_bypass_list.*
|
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.android.schedulers.AndroidSchedulers
|
||||||
import rx.schedulers.Schedulers
|
import rx.schedulers.Schedulers
|
||||||
import java.text.Collator
|
import java.text.Collator
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import android.view.inputmethod.EditorInfo
|
|
||||||
import android.view.inputmethod.InputMethodManager
|
|
||||||
import com.v2ray.ang.AppConfig
|
|
||||||
import com.v2ray.ang.dto.AppInfo
|
|
||||||
import com.v2ray.ang.extension.v2RayApplication
|
|
||||||
import com.v2ray.ang.util.Utils
|
|
||||||
import org.jetbrains.anko.doAsync
|
|
||||||
import org.jetbrains.anko.toast
|
|
||||||
import org.jetbrains.anko.uiThread
|
|
||||||
import java.net.URL
|
|
||||||
|
|
||||||
class PerAppProxyActivity : BaseActivity() {
|
class PerAppProxyActivity : BaseActivity() {
|
||||||
companion object {
|
private lateinit var binding: ActivityBypassListBinding
|
||||||
const val PREF_PER_APP_PROXY_SET = "pref_per_app_proxy_set"
|
|
||||||
const val PREF_BYPASS_APPS = "pref_bypass_apps"
|
|
||||||
}
|
|
||||||
|
|
||||||
private var adapter: PerAppProxyAdapter? = null
|
private var adapter: PerAppProxyAdapter? = null
|
||||||
private var appsAll: List<AppInfo>? = null
|
private var appsAll: List<AppInfo>? = null
|
||||||
|
private val defaultSharedPreferences by lazy { PreferenceManager.getDefaultSharedPreferences(this) }
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
setContentView(R.layout.activity_bypass_list)
|
binding = ActivityBypassListBinding.inflate(layoutInflater)
|
||||||
|
val view = binding.root
|
||||||
|
setContentView(view)
|
||||||
|
|
||||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||||
|
|
||||||
val dividerItemDecoration = LinearDividerItemDecoration(
|
val dividerItemDecoration = DividerItemDecoration(this, LinearLayoutManager.VERTICAL)
|
||||||
this, LinearDividerItemDecoration.LINEAR_DIVIDER_VERTICAL)
|
binding.recyclerView.addItemDecoration(dividerItemDecoration)
|
||||||
recycler_view.addItemDecoration(dividerItemDecoration)
|
|
||||||
|
|
||||||
val blacklist = defaultDPreference.getPrefStringSet(PREF_PER_APP_PROXY_SET, null)
|
val blacklist = defaultSharedPreferences.getStringSet(AppConfig.PREF_PER_APP_PROXY_SET, null)
|
||||||
|
|
||||||
AppManagerUtil.rxLoadNetworkAppList(this)
|
AppManagerUtil.rxLoadNetworkAppList(this)
|
||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
@@ -64,8 +59,8 @@ class PerAppProxyActivity : BaseActivity() {
|
|||||||
one.isSelected = 0
|
one.isSelected = 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val comparator = object : Comparator<AppInfo> {
|
val comparator = Comparator<AppInfo> { p1, p2 ->
|
||||||
override fun compare(p1: AppInfo, p2: AppInfo): Int = when {
|
when {
|
||||||
p1.isSelected > p2.isSelected -> -1
|
p1.isSelected > p2.isSelected -> -1
|
||||||
p1.isSelected == p2.isSelected -> 0
|
p1.isSelected == p2.isSelected -> 0
|
||||||
else -> 1
|
else -> 1
|
||||||
@@ -91,103 +86,123 @@ class PerAppProxyActivity : BaseActivity() {
|
|||||||
.subscribe {
|
.subscribe {
|
||||||
appsAll = it
|
appsAll = it
|
||||||
adapter = PerAppProxyAdapter(this, it, blacklist)
|
adapter = PerAppProxyAdapter(this, it, blacklist)
|
||||||
recycler_view.adapter = adapter
|
binding.recyclerView.adapter = adapter
|
||||||
pb_waiting.visibility = View.GONE
|
binding.pbWaiting.visibility = View.GONE
|
||||||
}
|
}
|
||||||
|
/***
|
||||||
recycler_view.addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
recycler_view.addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
||||||
var dst = 0
|
var dst = 0
|
||||||
val threshold = resources.getDimensionPixelSize(R.dimen.bypass_list_header_height) * 3
|
val threshold = resources.getDimensionPixelSize(R.dimen.bypass_list_header_height) * 2
|
||||||
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
||||||
dst += dy
|
dst += dy
|
||||||
if (dst > threshold) {
|
if (dst > threshold) {
|
||||||
header_view.hide()
|
header_view.hide()
|
||||||
dst = 0
|
dst = 0
|
||||||
} else if (dst < -20) {
|
} else if (dst < -20) {
|
||||||
header_view.show()
|
header_view.show()
|
||||||
dst = 0
|
dst = 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var hiding = false
|
var hiding = false
|
||||||
fun View.hide() {
|
fun View.hide() {
|
||||||
val target = -height.toFloat()
|
val target = -height.toFloat()
|
||||||
if (hiding || translationY == target) return
|
if (hiding || translationY == target) return
|
||||||
animate()
|
animate()
|
||||||
.translationY(target)
|
.translationY(target)
|
||||||
.setInterpolator(AccelerateInterpolator(2F))
|
.setInterpolator(AccelerateInterpolator(2F))
|
||||||
.setListener(object : AnimatorListenerAdapter() {
|
.setListener(object : AnimatorListenerAdapter() {
|
||||||
override fun onAnimationEnd(animation: Animator?) {
|
override fun onAnimationEnd(animation: Animator?) {
|
||||||
hiding = false
|
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
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
hiding = true
|
||||||
switch_per_app_proxy.setOnCheckedChangeListener { buttonView, isChecked ->
|
|
||||||
defaultDPreference.setPrefBoolean(SettingsActivity.PREF_PER_APP_PROXY, isChecked)
|
|
||||||
}
|
}
|
||||||
switch_per_app_proxy.isChecked = defaultDPreference.getPrefBoolean(SettingsActivity.PREF_PER_APP_PROXY, false)
|
|
||||||
|
|
||||||
switch_bypass_apps.setOnCheckedChangeListener { buttonView, isChecked ->
|
var showing = false
|
||||||
defaultDPreference.setPrefBoolean(PREF_BYPASS_APPS, isChecked)
|
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
|
||||||
}
|
}
|
||||||
switch_bypass_apps.isChecked = defaultDPreference.getPrefBoolean(PREF_BYPASS_APPS, 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 ->
|
et_search.setOnEditorActionListener { v, actionId, event ->
|
||||||
if (actionId == EditorInfo.IME_ACTION_SEARCH) {
|
if (actionId == EditorInfo.IME_ACTION_SEARCH) {
|
||||||
//hide
|
//hide
|
||||||
var imm: InputMethodManager = v.context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
var imm: InputMethodManager = v.context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||||
imm.toggleSoftInput(0, InputMethodManager.HIDE_NOT_ALWAYS)
|
imm.toggleSoftInput(0, InputMethodManager.HIDE_NOT_ALWAYS)
|
||||||
|
|
||||||
val key = v.text.toString().toUpperCase()
|
val key = v.text.toString().toUpperCase()
|
||||||
val apps = ArrayList<AppInfo>()
|
val apps = ArrayList<AppInfo>()
|
||||||
if (TextUtils.isEmpty(key)) {
|
if (TextUtils.isEmpty(key)) {
|
||||||
appsAll?.forEach {
|
appsAll?.forEach {
|
||||||
apps.add(it)
|
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
} 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() {
|
override fun onPause() {
|
||||||
super.onPause()
|
super.onPause()
|
||||||
adapter?.let {
|
adapter?.let {
|
||||||
defaultDPreference.setPrefStringSet(PREF_PER_APP_PROXY_SET, it.blacklist)
|
defaultSharedPreferences.edit().putStringSet(AppConfig.PREF_PER_APP_PROXY_SET, it.blacklist).apply()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
|
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||||
menuInflater.inflate(R.menu.menu_bypass_list, menu)
|
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)
|
return super.onCreateOptionsMenu(menu)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -204,14 +219,20 @@ class PerAppProxyActivity : BaseActivity() {
|
|||||||
val packageName = it.packageName
|
val packageName = it.packageName
|
||||||
adapter?.blacklist!!.add(packageName)
|
adapter?.blacklist!!.add(packageName)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
it.notifyDataSetChanged()
|
it.notifyDataSetChanged()
|
||||||
true
|
true
|
||||||
} ?: false
|
} ?: false
|
||||||
R.id.select_proxy_app -> {
|
R.id.select_proxy_app -> {
|
||||||
selectProxyApp()
|
selectProxyApp()
|
||||||
|
true
|
||||||
|
}
|
||||||
|
R.id.import_proxy_app -> {
|
||||||
|
importProxyApp()
|
||||||
|
true
|
||||||
|
}
|
||||||
|
R.id.export_proxy_app -> {
|
||||||
|
exportProxyApp()
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
else -> super.onOptionsItemSelected(item)
|
else -> super.onOptionsItemSelected(item)
|
||||||
@@ -220,22 +241,41 @@ class PerAppProxyActivity : BaseActivity() {
|
|||||||
private fun selectProxyApp() {
|
private fun selectProxyApp() {
|
||||||
toast(R.string.msg_downloading_content)
|
toast(R.string.msg_downloading_content)
|
||||||
val url = AppConfig.androidpackagenamelistUrl
|
val url = AppConfig.androidpackagenamelistUrl
|
||||||
doAsync {
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
val content = URL(url).readText()
|
val content = Utils.getUrlContext(url, 5000)
|
||||||
uiThread {
|
launch(Dispatchers.Main) {
|
||||||
Log.d("selectProxyApp", content)
|
Log.d(ANG_PACKAGE, content)
|
||||||
selectProxyApp(content)
|
selectProxyApp(content, true)
|
||||||
toast(R.string.toast_success)
|
toast(R.string.toast_success)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun selectProxyApp(content: String): Boolean {
|
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 {
|
try {
|
||||||
var proxyApps = content
|
val proxyApps = if (TextUtils.isEmpty(content)) {
|
||||||
if (TextUtils.isEmpty(content)) {
|
Utils.readTextFromAssets(v2RayApplication, "proxy_packagename.txt")
|
||||||
val assets = Utils.readTextFromAssets(v2RayApplication, "proxy_packagename.txt")
|
} else {
|
||||||
proxyApps = assets.lines().toString()
|
content
|
||||||
}
|
}
|
||||||
if (TextUtils.isEmpty(proxyApps)) {
|
if (TextUtils.isEmpty(proxyApps)) {
|
||||||
return false
|
return false
|
||||||
@@ -243,12 +283,12 @@ class PerAppProxyActivity : BaseActivity() {
|
|||||||
|
|
||||||
adapter?.blacklist!!.clear()
|
adapter?.blacklist!!.clear()
|
||||||
|
|
||||||
if (switch_bypass_apps.isChecked) {
|
if (binding.switchBypassApps.isChecked) {
|
||||||
adapter?.let {
|
adapter?.let {
|
||||||
it.apps.forEach block@{
|
it.apps.forEach block@{
|
||||||
val packageName = it.packageName
|
val packageName = it.packageName
|
||||||
Log.d("selectProxyApp2", packageName)
|
Log.d(ANG_PACKAGE, packageName)
|
||||||
if (proxyApps.indexOf(packageName) < 0) {
|
if (!inProxyApps(proxyApps, packageName, force)) {
|
||||||
adapter?.blacklist!!.add(packageName)
|
adapter?.blacklist!!.add(packageName)
|
||||||
println(packageName)
|
println(packageName)
|
||||||
return@block
|
return@block
|
||||||
@@ -260,8 +300,8 @@ class PerAppProxyActivity : BaseActivity() {
|
|||||||
adapter?.let {
|
adapter?.let {
|
||||||
it.apps.forEach block@{
|
it.apps.forEach block@{
|
||||||
val packageName = it.packageName
|
val packageName = it.packageName
|
||||||
Log.d("selectProxyApp3", packageName)
|
Log.d(ANG_PACKAGE, packageName)
|
||||||
if (proxyApps.indexOf(packageName) >= 0) {
|
if (inProxyApps(proxyApps, packageName, force)) {
|
||||||
adapter?.blacklist!!.add(packageName)
|
adapter?.blacklist!!.add(packageName)
|
||||||
println(packageName)
|
println(packageName)
|
||||||
return@block
|
return@block
|
||||||
@@ -276,4 +316,40 @@ class PerAppProxyActivity : BaseActivity() {
|
|||||||
}
|
}
|
||||||
return true
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,15 +1,12 @@
|
|||||||
package com.v2ray.ang.ui
|
package com.v2ray.ang.ui
|
||||||
|
|
||||||
import android.graphics.Color
|
import android.view.LayoutInflater
|
||||||
import android.support.v7.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import com.v2ray.ang.R
|
import com.v2ray.ang.R
|
||||||
|
import com.v2ray.ang.databinding.ItemRecyclerBypassListBinding
|
||||||
import com.v2ray.ang.dto.AppInfo
|
import com.v2ray.ang.dto.AppInfo
|
||||||
import kotlinx.android.synthetic.main.item_recycler_bypass_list.view.*
|
|
||||||
import org.jetbrains.anko.image
|
|
||||||
import org.jetbrains.anko.layoutInflater
|
|
||||||
import org.jetbrains.anko.textColor
|
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
class PerAppProxyAdapter(val activity: BaseActivity, val apps: List<AppInfo>, blacklist: MutableSet<String>?) :
|
class PerAppProxyAdapter(val activity: BaseActivity, val apps: List<AppInfo>, blacklist: MutableSet<String>?) :
|
||||||
@@ -20,8 +17,7 @@ class PerAppProxyAdapter(val activity: BaseActivity, val apps: List<AppInfo>, bl
|
|||||||
private const val VIEW_TYPE_ITEM = 1
|
private const val VIEW_TYPE_ITEM = 1
|
||||||
}
|
}
|
||||||
|
|
||||||
private var mActivity: BaseActivity = activity
|
val blacklist = if (blacklist == null) HashSet() else HashSet(blacklist)
|
||||||
val blacklist = if (blacklist == null) HashSet<String>() else HashSet<String>(blacklist)
|
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: BaseViewHolder, position: Int) {
|
override fun onBindViewHolder(holder: BaseViewHolder, position: Int) {
|
||||||
if (holder is AppViewHolder) {
|
if (holder is AppViewHolder) {
|
||||||
@@ -39,14 +35,13 @@ class PerAppProxyAdapter(val activity: BaseActivity, val apps: List<AppInfo>, bl
|
|||||||
VIEW_TYPE_HEADER -> {
|
VIEW_TYPE_HEADER -> {
|
||||||
val view = View(ctx)
|
val view = View(ctx)
|
||||||
view.layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
|
view.layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
|
||||||
ctx.resources.getDimensionPixelSize(R.dimen.bypass_list_header_height) * 3)
|
ctx.resources.getDimensionPixelSize(R.dimen.bypass_list_header_height) * 0)
|
||||||
BaseViewHolder(view)
|
BaseViewHolder(view)
|
||||||
}
|
}
|
||||||
// VIEW_TYPE_ITEM -> AppViewHolder(ctx.layoutInflater
|
// VIEW_TYPE_ITEM -> AppViewHolder(ctx.layoutInflater
|
||||||
// .inflate(R.layout.item_recycler_bypass_list, parent, false))
|
// .inflate(R.layout.item_recycler_bypass_list, parent, false))
|
||||||
|
|
||||||
else -> AppViewHolder(ctx.layoutInflater
|
else -> AppViewHolder(ItemRecyclerBypassListBinding.inflate(LayoutInflater.from(ctx), parent, false))
|
||||||
.inflate(R.layout.item_recycler_bypass_list, parent, false))
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -55,30 +50,25 @@ class PerAppProxyAdapter(val activity: BaseActivity, val apps: List<AppInfo>, bl
|
|||||||
|
|
||||||
open class BaseViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView)
|
open class BaseViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView)
|
||||||
|
|
||||||
inner class AppViewHolder(itemView: View) : BaseViewHolder(itemView),
|
inner class AppViewHolder(private val itemBypassBinding: ItemRecyclerBypassListBinding) : BaseViewHolder(itemBypassBinding.root),
|
||||||
View.OnClickListener {
|
View.OnClickListener {
|
||||||
private val inBlacklist: Boolean get() = blacklist.contains(appInfo.packageName)
|
private val inBlacklist: Boolean get() = blacklist.contains(appInfo.packageName)
|
||||||
private lateinit var appInfo: AppInfo
|
private lateinit var appInfo: AppInfo
|
||||||
|
|
||||||
val icon = itemView.icon!!
|
|
||||||
val name = itemView.name!!
|
|
||||||
val package_name = itemView.package_name!!
|
|
||||||
val checkBox = itemView.check_box!!
|
|
||||||
|
|
||||||
fun bind(appInfo: AppInfo) {
|
fun bind(appInfo: AppInfo) {
|
||||||
this.appInfo = appInfo
|
this.appInfo = appInfo
|
||||||
|
|
||||||
icon.image = appInfo.appIcon
|
itemBypassBinding.icon.setImageDrawable(appInfo.appIcon)
|
||||||
// name.text = appInfo.appName
|
// name.text = appInfo.appName
|
||||||
|
|
||||||
checkBox.isChecked = inBlacklist
|
itemBypassBinding.checkBox.isChecked = inBlacklist
|
||||||
package_name.text = appInfo.packageName
|
itemBypassBinding.packageName.text = appInfo.packageName
|
||||||
if (appInfo.isSystemApp) {
|
if (appInfo.isSystemApp) {
|
||||||
name.text = String.format("** %1s", appInfo.appName)
|
itemBypassBinding.name.text = String.format("** %1s", appInfo.appName)
|
||||||
name.textColor = Color.RED
|
//name.textColor = Color.RED
|
||||||
} else {
|
} else {
|
||||||
name.text = appInfo.appName
|
itemBypassBinding.name.text = appInfo.appName
|
||||||
name.textColor = Color.DKGRAY
|
//name.textColor = Color.DKGRAY
|
||||||
}
|
}
|
||||||
|
|
||||||
itemView.setOnClickListener(this)
|
itemView.setOnClickListener(this)
|
||||||
@@ -87,11 +77,11 @@ class PerAppProxyAdapter(val activity: BaseActivity, val apps: List<AppInfo>, bl
|
|||||||
override fun onClick(v: View?) {
|
override fun onClick(v: View?) {
|
||||||
if (inBlacklist) {
|
if (inBlacklist) {
|
||||||
blacklist.remove(appInfo.packageName)
|
blacklist.remove(appInfo.packageName)
|
||||||
checkBox.isChecked = false
|
itemBypassBinding.checkBox.isChecked = false
|
||||||
} else {
|
} else {
|
||||||
blacklist.add(appInfo.packageName)
|
blacklist.add(appInfo.packageName)
|
||||||
checkBox.isChecked = true
|
itemBypassBinding.checkBox.isChecked = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,23 +1,26 @@
|
|||||||
package com.v2ray.ang.ui
|
package com.v2ray.ang.ui
|
||||||
|
|
||||||
import android.graphics.Color
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import com.v2ray.ang.R
|
import com.v2ray.ang.R
|
||||||
import android.support.v4.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
|
import com.google.android.material.tabs.TabLayoutMediator
|
||||||
import com.v2ray.ang.AppConfig
|
import com.v2ray.ang.AppConfig
|
||||||
import kotlinx.android.synthetic.main.activity_routing_settings.*
|
import com.v2ray.ang.databinding.ActivityRoutingSettingsBinding
|
||||||
|
|
||||||
|
|
||||||
class RoutingSettingsActivity : BaseActivity() {
|
class RoutingSettingsActivity : BaseActivity() {
|
||||||
|
private lateinit var binding: ActivityRoutingSettingsBinding
|
||||||
|
|
||||||
private val titles: Array<out String> by lazy {
|
private val titles: Array<out String> by lazy {
|
||||||
resources.getStringArray(R.array.routing_tag)
|
resources.getStringArray(R.array.routing_tag)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
setContentView(R.layout.activity_routing_settings)
|
binding = ActivityRoutingSettingsBinding.inflate(layoutInflater)
|
||||||
|
val view = binding.root
|
||||||
|
setContentView(view)
|
||||||
|
|
||||||
title = getString(R.string.routing_settings_title)
|
title = getString(R.string.title_pref_routing_custom)
|
||||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||||
|
|
||||||
val fragments = ArrayList<Fragment>()
|
val fragments = ArrayList<Fragment>()
|
||||||
@@ -25,9 +28,11 @@ class RoutingSettingsActivity : BaseActivity() {
|
|||||||
fragments.add(RoutingSettingsFragment().newInstance(AppConfig.PREF_V2RAY_ROUTING_DIRECT))
|
fragments.add(RoutingSettingsFragment().newInstance(AppConfig.PREF_V2RAY_ROUTING_DIRECT))
|
||||||
fragments.add(RoutingSettingsFragment().newInstance(AppConfig.PREF_V2RAY_ROUTING_BLOCKED))
|
fragments.add(RoutingSettingsFragment().newInstance(AppConfig.PREF_V2RAY_ROUTING_BLOCKED))
|
||||||
|
|
||||||
val adapter = FragmentAdapter(supportFragmentManager, fragments, titles.toList())
|
val adapter = FragmentAdapter(this, fragments)
|
||||||
viewpager?.adapter = adapter
|
binding.viewpager.adapter = adapter
|
||||||
tablayout.setTabTextColors(Color.BLACK, Color.RED)
|
//tablayout.setTabTextColors(Color.BLACK, Color.RED)
|
||||||
tablayout.setupWithViewPager(viewpager)
|
TabLayoutMediator(binding.tablayout, binding.viewpager) { tab, position ->
|
||||||
|
tab.text = titles[position]
|
||||||
|
}.attach()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,41 +1,39 @@
|
|||||||
package com.v2ray.ang.ui
|
package com.v2ray.ang.ui
|
||||||
|
|
||||||
|
|
||||||
import android.Manifest
|
import android.Manifest
|
||||||
import android.app.Activity.RESULT_OK
|
import android.app.Activity.RESULT_OK
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.support.v4.app.Fragment
|
|
||||||
import android.text.TextUtils
|
import android.text.TextUtils
|
||||||
import android.util.Log
|
|
||||||
import android.view.*
|
import android.view.*
|
||||||
import com.v2ray.ang.R
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import com.v2ray.ang.extension.defaultDPreference
|
import androidx.fragment.app.Fragment
|
||||||
import com.v2ray.ang.util.Utils
|
import androidx.lifecycle.lifecycleScope
|
||||||
import kotlinx.android.synthetic.main.fragment_routing_settings.*
|
import androidx.preference.PreferenceManager
|
||||||
import org.jetbrains.anko.toast
|
|
||||||
import android.view.MenuInflater
|
|
||||||
import com.tbruyelle.rxpermissions.RxPermissions
|
import com.tbruyelle.rxpermissions.RxPermissions
|
||||||
import com.v2ray.ang.AppConfig
|
import com.v2ray.ang.AppConfig
|
||||||
import org.jetbrains.anko.doAsync
|
import com.v2ray.ang.R
|
||||||
import org.jetbrains.anko.startActivityForResult
|
import com.v2ray.ang.databinding.FragmentRoutingSettingsBinding
|
||||||
import org.jetbrains.anko.support.v4.startActivityForResult
|
import com.v2ray.ang.extension.toast
|
||||||
import org.jetbrains.anko.support.v4.toast
|
import com.v2ray.ang.extension.v2RayApplication
|
||||||
import org.jetbrains.anko.uiThread
|
import com.v2ray.ang.util.Utils
|
||||||
import java.net.URL
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.GlobalScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
class RoutingSettingsFragment : Fragment() {
|
class RoutingSettingsFragment : Fragment() {
|
||||||
|
private lateinit var binding: FragmentRoutingSettingsBinding
|
||||||
companion object {
|
companion object {
|
||||||
private const val routing_arg = "routing_arg"
|
private const val routing_arg = "routing_arg"
|
||||||
private const val REQUEST_SCAN_REPLACE = 11
|
|
||||||
private const val REQUEST_SCAN_APPEND = 12
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val defaultSharedPreferences by lazy { PreferenceManager.getDefaultSharedPreferences(requireContext()) }
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
|
||||||
savedInstanceState: Bundle?): View? {
|
savedInstanceState: Bundle?): View? {
|
||||||
// Inflate the layout for this fragment
|
// Inflate the layout for this fragment
|
||||||
return inflater.inflate(R.layout.fragment_routing_settings, container, false)
|
binding = FragmentRoutingSettingsBinding.inflate(layoutInflater)
|
||||||
|
return binding.root// inflater.inflate(R.layout.fragment_routing_settings, container, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun newInstance(arg: String): Fragment {
|
fun newInstance(arg: String): Fragment {
|
||||||
@@ -46,11 +44,11 @@ class RoutingSettingsFragment : Fragment() {
|
|||||||
return fragment
|
return fragment
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onActivityCreated(savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
val content = activity?.defaultDPreference?.getPrefString(arguments!!.getString(routing_arg), "")
|
val content = defaultSharedPreferences.getString(requireArguments().getString(routing_arg), "")
|
||||||
et_routing_content.text = Utils.getEditable(content!!)
|
binding.etRoutingContent.text = Utils.getEditable(content!!)
|
||||||
|
|
||||||
setHasOptionsMenu(true)
|
setHasOptionsMenu(true)
|
||||||
}
|
}
|
||||||
@@ -62,21 +60,19 @@ class RoutingSettingsFragment : Fragment() {
|
|||||||
|
|
||||||
override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
|
override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
|
||||||
R.id.save_routing -> {
|
R.id.save_routing -> {
|
||||||
val content = et_routing_content.text.toString()
|
saveRouting()
|
||||||
activity?.defaultDPreference?.setPrefString(arguments!!.getString(routing_arg), content)
|
|
||||||
activity?.toast(R.string.toast_success)
|
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
R.id.del_routing -> {
|
R.id.del_routing -> {
|
||||||
et_routing_content.text = null
|
binding.etRoutingContent.text = null
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
R.id.scan_replace -> {
|
R.id.scan_replace -> {
|
||||||
scanQRcode(REQUEST_SCAN_REPLACE)
|
scanQRcode(true)
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
R.id.scan_append -> {
|
R.id.scan_append -> {
|
||||||
scanQRcode(REQUEST_SCAN_APPEND)
|
scanQRcode(false)
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
R.id.default_rules -> {
|
R.id.default_rules -> {
|
||||||
@@ -86,17 +82,26 @@ class RoutingSettingsFragment : Fragment() {
|
|||||||
else -> super.onOptionsItemSelected(item)
|
else -> super.onOptionsItemSelected(item)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun scanQRcode(requestCode: Int): Boolean {
|
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 {
|
// try {
|
||||||
// startActivityForResult(Intent("com.google.zxing.client.android.SCAN")
|
// startActivityForResult(Intent("com.google.zxing.client.android.SCAN")
|
||||||
// .addCategory(Intent.CATEGORY_DEFAULT)
|
// .addCategory(Intent.CATEGORY_DEFAULT)
|
||||||
// .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP), requestCode)
|
// .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP), requestCode)
|
||||||
// } catch (e: Exception) {
|
// } catch (e: Exception) {
|
||||||
RxPermissions(activity!!)
|
RxPermissions(requireActivity())
|
||||||
.request(Manifest.permission.CAMERA)
|
.request(Manifest.permission.CAMERA)
|
||||||
.subscribe {
|
.subscribe {
|
||||||
if (it)
|
if (it)
|
||||||
startActivityForResult<ScannerActivity>(requestCode)
|
if (forReplace)
|
||||||
|
scanQRCodeForReplace.launch(Intent(activity, ScannerActivity::class.java))
|
||||||
|
else
|
||||||
|
scanQRCodeForAppend.launch(Intent(activity, ScannerActivity::class.java))
|
||||||
else
|
else
|
||||||
activity?.toast(R.string.toast_permission_denied)
|
activity?.toast(R.string.toast_permission_denied)
|
||||||
}
|
}
|
||||||
@@ -104,46 +109,50 @@ class RoutingSettingsFragment : Fragment() {
|
|||||||
return true
|
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 {
|
fun setDefaultRules(): Boolean {
|
||||||
var url = AppConfig.v2rayCustomRoutingListUrl
|
var url = AppConfig.v2rayCustomRoutingListUrl
|
||||||
when (arguments!!.getString(routing_arg)) {
|
var tag = ""
|
||||||
|
when (requireArguments().getString(routing_arg)) {
|
||||||
AppConfig.PREF_V2RAY_ROUTING_AGENT -> {
|
AppConfig.PREF_V2RAY_ROUTING_AGENT -> {
|
||||||
url += AppConfig.TAG_AGENT
|
tag = AppConfig.TAG_AGENT
|
||||||
}
|
}
|
||||||
AppConfig.PREF_V2RAY_ROUTING_DIRECT -> {
|
AppConfig.PREF_V2RAY_ROUTING_DIRECT -> {
|
||||||
url += AppConfig.TAG_DIRECT
|
tag = AppConfig.TAG_DIRECT
|
||||||
}
|
}
|
||||||
AppConfig.PREF_V2RAY_ROUTING_BLOCKED -> {
|
AppConfig.PREF_V2RAY_ROUTING_BLOCKED -> {
|
||||||
url += AppConfig.TAG_BLOCKED
|
tag = AppConfig.TAG_BLOCKED
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
url += tag
|
||||||
|
|
||||||
toast(R.string.msg_downloading_content)
|
activity?.toast(R.string.msg_downloading_content)
|
||||||
doAsync {
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
val content = URL(url).readText()
|
val content = Utils.getUrlContext(url, 5000)
|
||||||
uiThread {
|
launch(Dispatchers.Main) {
|
||||||
et_routing_content.text = Utils.getEditable(content!!)
|
val routingList = if (TextUtils.isEmpty(content)) {
|
||||||
toast(R.string.toast_success)
|
Utils.readTextFromAssets(activity?.v2RayApplication!!, "custom_routing_$tag")
|
||||||
|
} else {
|
||||||
|
content
|
||||||
|
}
|
||||||
|
binding.etRoutingContent.text = Utils.getEditable(routingList)
|
||||||
|
saveRouting()
|
||||||
|
//toast(R.string.toast_success)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
|
||||||
super.onActivityResult(requestCode, resultCode, data)
|
|
||||||
when (requestCode) {
|
|
||||||
REQUEST_SCAN_REPLACE ->
|
|
||||||
if (resultCode == RESULT_OK) {
|
|
||||||
val content = data?.getStringExtra("SCAN_RESULT")
|
|
||||||
et_routing_content.text = Utils.getEditable(content!!)
|
|
||||||
}
|
|
||||||
REQUEST_SCAN_APPEND ->
|
|
||||||
if (resultCode == RESULT_OK) {
|
|
||||||
val content = data?.getStringExtra("SCAN_RESULT")
|
|
||||||
et_routing_content.text = Utils.getEditable("${et_routing_content.text},$content")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,25 +6,23 @@ import com.tbruyelle.rxpermissions.RxPermissions
|
|||||||
import com.v2ray.ang.R
|
import com.v2ray.ang.R
|
||||||
import com.v2ray.ang.util.AngConfigManager
|
import com.v2ray.ang.util.AngConfigManager
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import org.jetbrains.anko.*
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import com.v2ray.ang.extension.toast
|
||||||
|
|
||||||
class ScScannerActivity : BaseActivity() {
|
class ScScannerActivity : BaseActivity() {
|
||||||
companion object {
|
|
||||||
private const val REQUEST_SCAN = 1
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
setContentView(R.layout.activity_none)
|
setContentView(R.layout.activity_none)
|
||||||
importQRcode(REQUEST_SCAN)
|
importQRcode()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun importQRcode(requestCode: Int): Boolean {
|
fun importQRcode(): Boolean {
|
||||||
RxPermissions(this)
|
RxPermissions(this)
|
||||||
.request(Manifest.permission.CAMERA)
|
.request(Manifest.permission.CAMERA)
|
||||||
.subscribe {
|
.subscribe {
|
||||||
if (it)
|
if (it)
|
||||||
startActivityForResult<ScannerActivity>(requestCode)
|
scanQRCode.launch(Intent(this, ScannerActivity::class.java))
|
||||||
else
|
else
|
||||||
toast(R.string.toast_permission_denied)
|
toast(R.string.toast_permission_denied)
|
||||||
}
|
}
|
||||||
@@ -32,21 +30,16 @@ class ScScannerActivity : BaseActivity() {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
private val scanQRCode = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||||
super.onActivityResult(requestCode, resultCode, data)
|
if (it.resultCode == RESULT_OK) {
|
||||||
when (requestCode) {
|
val count = AngConfigManager.importBatchConfig(it.data?.getStringExtra("SCAN_RESULT"), "", false)
|
||||||
REQUEST_SCAN ->
|
if (count > 0) {
|
||||||
if (resultCode == RESULT_OK) {
|
toast(R.string.toast_success)
|
||||||
val count = AngConfigManager.importBatchConfig(data?.getStringExtra("SCAN_RESULT"), "")
|
} else {
|
||||||
if (count > 0) {
|
toast(R.string.toast_failure)
|
||||||
toast(R.string.toast_success)
|
}
|
||||||
} else {
|
startActivity(Intent(this, MainActivity::class.java))
|
||||||
toast(R.string.toast_failure)
|
|
||||||
}
|
|
||||||
startActivity<MainActivity>()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,104 +1,22 @@
|
|||||||
package com.v2ray.ang.ui
|
package com.v2ray.ang.ui
|
||||||
|
|
||||||
import android.content.*
|
|
||||||
import android.net.VpnService
|
|
||||||
import com.v2ray.ang.R
|
import com.v2ray.ang.R
|
||||||
import com.v2ray.ang.util.Utils
|
import com.v2ray.ang.util.Utils
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import com.v2ray.ang.AppConfig
|
import com.v2ray.ang.service.V2RayServiceManager
|
||||||
import com.v2ray.ang.util.MessageUtil
|
|
||||||
import java.lang.ref.SoftReference
|
|
||||||
import android.content.IntentFilter
|
|
||||||
import kotlinx.android.synthetic.main.activity_main.*
|
|
||||||
import rx.Observable
|
|
||||||
import rx.android.schedulers.AndroidSchedulers
|
|
||||||
import java.util.concurrent.TimeUnit
|
|
||||||
|
|
||||||
class ScSwitchActivity : BaseActivity() {
|
class ScSwitchActivity : BaseActivity() {
|
||||||
companion object {
|
|
||||||
private const val REQUEST_CODE_VPN_PREPARE = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
var isRunning = false
|
|
||||||
set(value) {
|
|
||||||
field = value
|
|
||||||
if (value) {
|
|
||||||
Utils.stopVService(this)
|
|
||||||
} else {
|
|
||||||
val intent = VpnService.prepare(this)
|
|
||||||
if (intent == null) {
|
|
||||||
Utils.startVService(this)
|
|
||||||
} else {
|
|
||||||
startActivityForResult(intent, REQUEST_CODE_VPN_PREPARE)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
finishActivity()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun finishActivity() {
|
|
||||||
try {
|
|
||||||
Observable.timer(5000, TimeUnit.MILLISECONDS)
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.subscribe {
|
|
||||||
finish()
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
moveTaskToBack(true)
|
moveTaskToBack(true)
|
||||||
|
|
||||||
setContentView(R.layout.activity_none)
|
setContentView(R.layout.activity_none)
|
||||||
|
|
||||||
val isRunning = Utils.isServiceRun(this, "com.v2ray.ang.service.V2RayVpnService")
|
if (V2RayServiceManager.v2rayPoint.isRunning) {
|
||||||
if (isRunning) {
|
Utils.stopVService(this)
|
||||||
//Utils.stopVService(this)
|
|
||||||
mMsgReceive = ReceiveMessageHandler(this@ScSwitchActivity)
|
|
||||||
registerReceiver(mMsgReceive, IntentFilter(AppConfig.BROADCAST_ACTION_ACTIVITY))
|
|
||||||
MessageUtil.sendMsg2Service(this, AppConfig.MSG_REGISTER_CLIENT, "")
|
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
Utils.startVService(this)
|
Utils.startVServiceFromToggle(this)
|
||||||
finishActivity()
|
|
||||||
}
|
}
|
||||||
|
finish()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
override fun onStop() {
|
|
||||||
super.onStop()
|
|
||||||
if (mMsgReceive != null) {
|
|
||||||
unregisterReceiver(mMsgReceive)
|
|
||||||
mMsgReceive = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private var mMsgReceive: BroadcastReceiver? = null
|
|
||||||
|
|
||||||
private class ReceiveMessageHandler(activity: ScSwitchActivity) : BroadcastReceiver() {
|
|
||||||
internal var mReference: SoftReference<ScSwitchActivity> = SoftReference(activity)
|
|
||||||
override fun onReceive(ctx: Context?, intent: Intent?) {
|
|
||||||
val activity = mReference.get()
|
|
||||||
when (intent?.getIntExtra("key", 0)) {
|
|
||||||
AppConfig.MSG_STATE_RUNNING -> {
|
|
||||||
activity?.isRunning = true
|
|
||||||
}
|
|
||||||
AppConfig.MSG_STATE_NOT_RUNNING -> {
|
|
||||||
activity?.isRunning = false
|
|
||||||
}
|
|
||||||
// AppConfig.MSG_STATE_START_SUCCESS -> {
|
|
||||||
// activity?.toast(R.string.toast_services_success)
|
|
||||||
// activity?.isRunning = true
|
|
||||||
// }
|
|
||||||
// AppConfig.MSG_STATE_START_FAILURE -> {
|
|
||||||
// activity?.toast(R.string.toast_services_failure)
|
|
||||||
// activity?.isRunning = false
|
|
||||||
// }
|
|
||||||
// AppConfig.MSG_STATE_STOP_SUCCESS -> {
|
|
||||||
// activity?.isRunning = false
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -7,34 +7,21 @@ import com.google.zxing.Result
|
|||||||
import me.dm7.barcodescanner.zxing.ZXingScannerView
|
import me.dm7.barcodescanner.zxing.ZXingScannerView
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.graphics.BitmapFactory
|
import android.graphics.BitmapFactory
|
||||||
import android.icu.util.TimeUnit
|
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import com.google.zxing.BarcodeFormat
|
import com.google.zxing.BarcodeFormat
|
||||||
import com.tbruyelle.rxpermissions.RxPermissions
|
import com.tbruyelle.rxpermissions.RxPermissions
|
||||||
import com.v2ray.ang.R
|
import com.v2ray.ang.R
|
||||||
|
import com.v2ray.ang.extension.toast
|
||||||
import com.v2ray.ang.util.QRCodeDecoder
|
import com.v2ray.ang.util.QRCodeDecoder
|
||||||
import org.jetbrains.anko.toast
|
|
||||||
import rx.Observable
|
|
||||||
import android.os.SystemClock
|
|
||||||
import kotlinx.android.synthetic.main.activity_main.*
|
|
||||||
import rx.Observer
|
|
||||||
import rx.android.schedulers.AndroidSchedulers
|
|
||||||
import javax.xml.datatype.DatatypeConstants.SECONDS
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class ScannerActivity : BaseActivity(), ZXingScannerView.ResultHandler {
|
class ScannerActivity : BaseActivity(), ZXingScannerView.ResultHandler {
|
||||||
companion object {
|
|
||||||
private const val REQUEST_FILE_CHOOSER = 2
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private var mScannerView: ZXingScannerView? = null
|
private var mScannerView: ZXingScannerView? = null
|
||||||
|
|
||||||
public override fun onCreate(state: Bundle?) {
|
public override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(state)
|
super.onCreate(savedInstanceState)
|
||||||
mScannerView = ZXingScannerView(this) // Programmatically initialize the scanner view
|
mScannerView = ZXingScannerView(this) // Programmatically initialize the scanner view
|
||||||
|
|
||||||
mScannerView?.setAutoFocus(true)
|
mScannerView?.setAutoFocus(true)
|
||||||
@@ -69,14 +56,14 @@ class ScannerActivity : BaseActivity(), ZXingScannerView.ResultHandler {
|
|||||||
// mScannerView!!.resumeCameraPreview(this)
|
// mScannerView!!.resumeCameraPreview(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun finished(text: String) {
|
private fun finished(text: String) {
|
||||||
val intent = Intent()
|
val intent = Intent()
|
||||||
intent.putExtra("SCAN_RESULT", text)
|
intent.putExtra("SCAN_RESULT", text)
|
||||||
setResult(Activity.RESULT_OK, intent)
|
setResult(Activity.RESULT_OK, intent)
|
||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
|
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||||
menuInflater.inflate(R.menu.menu_scanner, menu)
|
menuInflater.inflate(R.menu.menu_scanner, menu)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -107,30 +94,23 @@ class ScannerActivity : BaseActivity(), ZXingScannerView.ResultHandler {
|
|||||||
//intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)
|
//intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
startActivityForResult(
|
chooseFile.launch(Intent.createChooser(intent, getString(R.string.title_file_chooser)))
|
||||||
Intent.createChooser(intent, getString(R.string.title_file_chooser)),
|
|
||||||
REQUEST_FILE_CHOOSER)
|
|
||||||
} catch (ex: android.content.ActivityNotFoundException) {
|
} catch (ex: android.content.ActivityNotFoundException) {
|
||||||
toast(R.string.toast_require_file_manager)
|
toast(R.string.toast_require_file_manager)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val chooseFile = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
val uri = it.data?.data
|
||||||
super.onActivityResult(requestCode, resultCode, data)
|
if (it.resultCode == RESULT_OK && uri != null) {
|
||||||
when (requestCode) {
|
try {
|
||||||
REQUEST_FILE_CHOOSER ->
|
val bitmap = BitmapFactory.decodeStream(contentResolver.openInputStream(uri))
|
||||||
if (resultCode == RESULT_OK) {
|
val text = QRCodeDecoder.syncDecodeQRCode(bitmap)
|
||||||
try {
|
finished(text!!)
|
||||||
val uri = data!!.data
|
} catch (e: Exception) {
|
||||||
val bitmap = BitmapFactory.decodeStream(contentResolver.openInputStream(uri))
|
e.printStackTrace()
|
||||||
val text = QRCodeDecoder.syncDecodeQRCode(bitmap)
|
toast(e.message.toString())
|
||||||
finished(text)
|
}
|
||||||
} catch (e: Exception) {
|
|
||||||
e.printStackTrace()
|
|
||||||
toast(e.message.toString())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,161 +0,0 @@
|
|||||||
package com.v2ray.ang.ui
|
|
||||||
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.text.Editable
|
|
||||||
import android.text.TextUtils
|
|
||||||
import android.view.Menu
|
|
||||||
import android.view.MenuItem
|
|
||||||
import com.google.gson.Gson
|
|
||||||
import com.v2ray.ang.AppConfig
|
|
||||||
import com.v2ray.ang.R
|
|
||||||
import com.v2ray.ang.extension.defaultDPreference
|
|
||||||
import com.v2ray.ang.dto.AngConfig
|
|
||||||
import com.v2ray.ang.util.AngConfigManager
|
|
||||||
import com.v2ray.ang.util.Utils
|
|
||||||
import kotlinx.android.synthetic.main.activity_server2.*
|
|
||||||
import org.jetbrains.anko.*
|
|
||||||
import java.lang.Exception
|
|
||||||
|
|
||||||
|
|
||||||
class Server2Activity : BaseActivity() {
|
|
||||||
companion object {
|
|
||||||
private const val REQUEST_SCAN = 1
|
|
||||||
}
|
|
||||||
|
|
||||||
var del_config: MenuItem? = null
|
|
||||||
var save_config: MenuItem? = null
|
|
||||||
|
|
||||||
private lateinit var configs: AngConfig
|
|
||||||
private var edit_index: Int = -1 //当前编辑的服务器
|
|
||||||
private var edit_guid: String = ""
|
|
||||||
private var isRunning: Boolean = false
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
setContentView(R.layout.activity_server2)
|
|
||||||
|
|
||||||
configs = AngConfigManager.configs
|
|
||||||
edit_index = intent.getIntExtra("position", -1)
|
|
||||||
isRunning = intent.getBooleanExtra("isRunning", false)
|
|
||||||
title = getString(R.string.title_server)
|
|
||||||
|
|
||||||
if (edit_index >= 0) {
|
|
||||||
edit_guid = configs.vmess[edit_index].guid
|
|
||||||
bindingServer(configs.vmess[edit_index])
|
|
||||||
} else {
|
|
||||||
clearServer()
|
|
||||||
}
|
|
||||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* bingding seleced server config
|
|
||||||
*/
|
|
||||||
fun bindingServer(vmess: AngConfig.VmessBean): Boolean {
|
|
||||||
et_remarks.text = Utils.getEditable(vmess.remarks)
|
|
||||||
tv_content.text = Editable.Factory.getInstance().newEditable(defaultDPreference.getPrefString(AppConfig.ANG_CONFIG + edit_guid, ""))
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* clear or init server config
|
|
||||||
*/
|
|
||||||
fun clearServer(): Boolean {
|
|
||||||
et_remarks.text = null
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* save server config
|
|
||||||
*/
|
|
||||||
fun saveServer(): Boolean {
|
|
||||||
var saveSuccess: Boolean
|
|
||||||
val vmess = configs.vmess[edit_index]
|
|
||||||
|
|
||||||
vmess.remarks = et_remarks.text.toString()
|
|
||||||
|
|
||||||
if (TextUtils.isEmpty(vmess.remarks)) {
|
|
||||||
toast(R.string.server_lab_remarks)
|
|
||||||
saveSuccess = false
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
if (AngConfigManager.addCustomServer(vmess, edit_index) == 0) {
|
|
||||||
toast(R.string.toast_success)
|
|
||||||
saveSuccess = true
|
|
||||||
} else {
|
|
||||||
toast(R.string.toast_failure)
|
|
||||||
saveSuccess = false
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
try {
|
|
||||||
Gson().fromJson<Object>(tv_content.text.toString(), Object::class.java)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
e.printStackTrace()
|
|
||||||
toast(R.string.toast_malformed_josn)
|
|
||||||
saveSuccess = false
|
|
||||||
}
|
|
||||||
|
|
||||||
if (saveSuccess) {
|
|
||||||
//update config
|
|
||||||
defaultDPreference.setPrefString(AppConfig.ANG_CONFIG + edit_guid, tv_content.text.toString())
|
|
||||||
finish()
|
|
||||||
return true
|
|
||||||
} else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* save server config
|
|
||||||
*/
|
|
||||||
fun deleteServer(): Boolean {
|
|
||||||
if (edit_index >= 0) {
|
|
||||||
alert(R.string.del_config_comfirm) {
|
|
||||||
positiveButton(android.R.string.ok) {
|
|
||||||
if (AngConfigManager.removeServer(edit_index) == 0) {
|
|
||||||
toast(R.string.toast_success)
|
|
||||||
finish()
|
|
||||||
} else {
|
|
||||||
toast(R.string.toast_failure)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
show()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
}
|
|
||||||
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 (edit_index >= 0) {
|
|
||||||
if (isRunning) {
|
|
||||||
if (edit_index == configs.index) {
|
|
||||||
del_config?.isVisible = false
|
|
||||||
save_config?.isVisible = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
del_config?.isVisible = false
|
|
||||||
}
|
|
||||||
|
|
||||||
return super.onCreateOptionsMenu(menu)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
|
|
||||||
R.id.del_config -> {
|
|
||||||
deleteServer()
|
|
||||||
true
|
|
||||||
}
|
|
||||||
R.id.save_config -> {
|
|
||||||
saveServer()
|
|
||||||
true
|
|
||||||
}
|
|
||||||
else -> super.onOptionsItemSelected(item)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,175 +0,0 @@
|
|||||||
package com.v2ray.ang.ui
|
|
||||||
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.text.TextUtils
|
|
||||||
import android.view.Menu
|
|
||||||
import android.view.MenuItem
|
|
||||||
import com.v2ray.ang.R
|
|
||||||
import com.v2ray.ang.dto.AngConfig
|
|
||||||
import com.v2ray.ang.util.AngConfigManager
|
|
||||||
import com.v2ray.ang.util.Utils
|
|
||||||
import kotlinx.android.synthetic.main.activity_server3.*
|
|
||||||
import org.jetbrains.anko.*
|
|
||||||
|
|
||||||
|
|
||||||
class Server3Activity : BaseActivity() {
|
|
||||||
companion object {
|
|
||||||
private const val REQUEST_SCAN = 1
|
|
||||||
}
|
|
||||||
|
|
||||||
var del_config: MenuItem? = null
|
|
||||||
var save_config: MenuItem? = null
|
|
||||||
|
|
||||||
private lateinit var configs: AngConfig
|
|
||||||
private var edit_index: Int = -1 //当前编辑的服务器
|
|
||||||
private var edit_guid: String = ""
|
|
||||||
private var isRunning: Boolean = false
|
|
||||||
private val securitys: Array<out String> by lazy {
|
|
||||||
resources.getStringArray(R.array.ss_securitys)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
setContentView(R.layout.activity_server3)
|
|
||||||
|
|
||||||
configs = AngConfigManager.configs
|
|
||||||
edit_index = intent.getIntExtra("position", -1)
|
|
||||||
isRunning = intent.getBooleanExtra("isRunning", false)
|
|
||||||
title = getString(R.string.title_server)
|
|
||||||
|
|
||||||
if (edit_index >= 0) {
|
|
||||||
edit_guid = configs.vmess[edit_index].guid
|
|
||||||
bindingServer(configs.vmess[edit_index])
|
|
||||||
} else {
|
|
||||||
clearServer()
|
|
||||||
}
|
|
||||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* bingding seleced server config
|
|
||||||
*/
|
|
||||||
fun bindingServer(vmess: AngConfig.VmessBean): Boolean {
|
|
||||||
et_remarks.text = Utils.getEditable(vmess.remarks)
|
|
||||||
|
|
||||||
et_address.text = Utils.getEditable(vmess.address)
|
|
||||||
et_port.text = Utils.getEditable(vmess.port.toString())
|
|
||||||
et_id.text = Utils.getEditable(vmess.id)
|
|
||||||
val security = Utils.arrayFind(securitys, vmess.security)
|
|
||||||
if (security >= 0) {
|
|
||||||
sp_security.setSelection(security)
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* clear or init server config
|
|
||||||
*/
|
|
||||||
fun clearServer(): Boolean {
|
|
||||||
et_remarks.text = null
|
|
||||||
et_address.text = null
|
|
||||||
et_port.text = Utils.getEditable("10086")
|
|
||||||
et_id.text = null
|
|
||||||
sp_security.setSelection(0)
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* save server config
|
|
||||||
*/
|
|
||||||
fun saveServer(): Boolean {
|
|
||||||
val vmess: AngConfig.VmessBean
|
|
||||||
if (edit_index >= 0) {
|
|
||||||
vmess = configs.vmess[edit_index]
|
|
||||||
} else {
|
|
||||||
vmess = AngConfig.VmessBean()
|
|
||||||
}
|
|
||||||
|
|
||||||
vmess.guid = edit_guid
|
|
||||||
vmess.remarks = et_remarks.text.toString()
|
|
||||||
vmess.address = et_address.text.toString()
|
|
||||||
vmess.port = Utils.parseInt(et_port.text.toString())
|
|
||||||
vmess.id = et_id.text.toString()
|
|
||||||
vmess.security = securitys[sp_security.selectedItemPosition]
|
|
||||||
|
|
||||||
if (TextUtils.isEmpty(vmess.remarks)) {
|
|
||||||
toast(R.string.server_lab_remarks)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if (TextUtils.isEmpty(vmess.address)) {
|
|
||||||
toast(R.string.server_lab_address3)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if (TextUtils.isEmpty(vmess.port.toString()) || vmess.port <= 0) {
|
|
||||||
toast(R.string.server_lab_port3)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if (TextUtils.isEmpty(vmess.id)) {
|
|
||||||
toast(R.string.server_lab_id3)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if (AngConfigManager.addShadowsocksServer(vmess, edit_index) == 0) {
|
|
||||||
toast(R.string.toast_success)
|
|
||||||
finish()
|
|
||||||
return true
|
|
||||||
} else {
|
|
||||||
toast(R.string.toast_failure)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* save server config
|
|
||||||
*/
|
|
||||||
fun deleteServer(): Boolean {
|
|
||||||
if (edit_index >= 0) {
|
|
||||||
alert(R.string.del_config_comfirm) {
|
|
||||||
positiveButton(android.R.string.ok) {
|
|
||||||
if (AngConfigManager.removeServer(edit_index) == 0) {
|
|
||||||
toast(R.string.toast_success)
|
|
||||||
finish()
|
|
||||||
} else {
|
|
||||||
toast(R.string.toast_failure)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
show()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
}
|
|
||||||
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 (edit_index >= 0) {
|
|
||||||
if (isRunning) {
|
|
||||||
if (edit_index == configs.index) {
|
|
||||||
del_config?.isVisible = false
|
|
||||||
save_config?.isVisible = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
del_config?.isVisible = false
|
|
||||||
}
|
|
||||||
|
|
||||||
return super.onCreateOptionsMenu(menu)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
|
|
||||||
R.id.del_config -> {
|
|
||||||
deleteServer()
|
|
||||||
true
|
|
||||||
}
|
|
||||||
R.id.save_config -> {
|
|
||||||
saveServer()
|
|
||||||
true
|
|
||||||
}
|
|
||||||
else -> super.onOptionsItemSelected(item)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,159 +0,0 @@
|
|||||||
package com.v2ray.ang.ui
|
|
||||||
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.text.TextUtils
|
|
||||||
import android.view.Menu
|
|
||||||
import android.view.MenuItem
|
|
||||||
import com.v2ray.ang.R
|
|
||||||
import com.v2ray.ang.dto.AngConfig
|
|
||||||
import com.v2ray.ang.util.AngConfigManager
|
|
||||||
import com.v2ray.ang.util.Utils
|
|
||||||
import kotlinx.android.synthetic.main.activity_server4.*
|
|
||||||
import org.jetbrains.anko.*
|
|
||||||
|
|
||||||
|
|
||||||
class Server4Activity : BaseActivity() {
|
|
||||||
companion object {
|
|
||||||
private const val REQUEST_SCAN = 1
|
|
||||||
}
|
|
||||||
|
|
||||||
var del_config: MenuItem? = null
|
|
||||||
var save_config: MenuItem? = null
|
|
||||||
|
|
||||||
private lateinit var configs: AngConfig
|
|
||||||
private var edit_index: Int = -1 //当前编辑的服务器
|
|
||||||
private var edit_guid: String = ""
|
|
||||||
private var isRunning: Boolean = false
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
setContentView(R.layout.activity_server4)
|
|
||||||
|
|
||||||
configs = AngConfigManager.configs
|
|
||||||
edit_index = intent.getIntExtra("position", -1)
|
|
||||||
isRunning = intent.getBooleanExtra("isRunning", false)
|
|
||||||
title = getString(R.string.title_server)
|
|
||||||
|
|
||||||
if (edit_index >= 0) {
|
|
||||||
edit_guid = configs.vmess[edit_index].guid
|
|
||||||
bindingServer(configs.vmess[edit_index])
|
|
||||||
} else {
|
|
||||||
clearServer()
|
|
||||||
}
|
|
||||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* bingding seleced server config
|
|
||||||
*/
|
|
||||||
fun bindingServer(vmess: AngConfig.VmessBean): Boolean {
|
|
||||||
et_remarks.text = Utils.getEditable(vmess.remarks)
|
|
||||||
|
|
||||||
et_address.text = Utils.getEditable(vmess.address)
|
|
||||||
et_port.text = Utils.getEditable(vmess.port.toString())
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* clear or init server config
|
|
||||||
*/
|
|
||||||
fun clearServer(): Boolean {
|
|
||||||
et_remarks.text = null
|
|
||||||
et_address.text = null
|
|
||||||
et_port.text = Utils.getEditable("10086")
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* save server config
|
|
||||||
*/
|
|
||||||
fun saveServer(): Boolean {
|
|
||||||
val vmess: AngConfig.VmessBean
|
|
||||||
if (edit_index >= 0) {
|
|
||||||
vmess = configs.vmess[edit_index]
|
|
||||||
} else {
|
|
||||||
vmess = AngConfig.VmessBean()
|
|
||||||
}
|
|
||||||
|
|
||||||
vmess.guid = edit_guid
|
|
||||||
vmess.remarks = et_remarks.text.toString()
|
|
||||||
vmess.address = et_address.text.toString()
|
|
||||||
vmess.port = Utils.parseInt(et_port.text.toString())
|
|
||||||
|
|
||||||
if (TextUtils.isEmpty(vmess.remarks)) {
|
|
||||||
toast(R.string.server_lab_remarks)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if (TextUtils.isEmpty(vmess.address)) {
|
|
||||||
toast(R.string.server_lab_address3)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if (TextUtils.isEmpty(vmess.port.toString()) || vmess.port <= 0) {
|
|
||||||
toast(R.string.server_lab_port3)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if (AngConfigManager.addSocksServer(vmess, edit_index) == 0) {
|
|
||||||
toast(R.string.toast_success)
|
|
||||||
finish()
|
|
||||||
return true
|
|
||||||
} else {
|
|
||||||
toast(R.string.toast_failure)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* save server config
|
|
||||||
*/
|
|
||||||
fun deleteServer(): Boolean {
|
|
||||||
if (edit_index >= 0) {
|
|
||||||
alert(R.string.del_config_comfirm) {
|
|
||||||
positiveButton(android.R.string.ok) {
|
|
||||||
if (AngConfigManager.removeServer(edit_index) == 0) {
|
|
||||||
toast(R.string.toast_success)
|
|
||||||
finish()
|
|
||||||
} else {
|
|
||||||
toast(R.string.toast_failure)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
show()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
}
|
|
||||||
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 (edit_index >= 0) {
|
|
||||||
if (isRunning) {
|
|
||||||
if (edit_index == configs.index) {
|
|
||||||
del_config?.isVisible = false
|
|
||||||
save_config?.isVisible = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -4,51 +4,132 @@ import android.os.Bundle
|
|||||||
import android.text.TextUtils
|
import android.text.TextUtils
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.*
|
||||||
|
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.R
|
||||||
import com.v2ray.ang.dto.AngConfig
|
import com.v2ray.ang.dto.EConfigType
|
||||||
import com.v2ray.ang.util.AngConfigManager
|
import com.v2ray.ang.dto.ServerConfig
|
||||||
|
import com.v2ray.ang.dto.V2rayConfig
|
||||||
|
import com.v2ray.ang.dto.V2rayConfig.Companion.DEFAULT_FLOW
|
||||||
|
import com.v2ray.ang.dto.V2rayConfig.Companion.DEFAULT_PORT
|
||||||
|
import com.v2ray.ang.dto.V2rayConfig.Companion.XTLS
|
||||||
|
import com.v2ray.ang.extension.toast
|
||||||
|
import com.v2ray.ang.util.MmkvManager
|
||||||
|
import com.v2ray.ang.util.MmkvManager.ID_MAIN
|
||||||
|
import com.v2ray.ang.util.MmkvManager.KEY_SELECTED_SERVER
|
||||||
import com.v2ray.ang.util.Utils
|
import com.v2ray.ang.util.Utils
|
||||||
import kotlinx.android.synthetic.main.activity_server.*
|
|
||||||
import org.jetbrains.anko.*
|
|
||||||
|
|
||||||
|
|
||||||
class ServerActivity : BaseActivity() {
|
class ServerActivity : BaseActivity() {
|
||||||
companion object {
|
|
||||||
private const val REQUEST_SCAN = 1
|
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")
|
||||||
}
|
}
|
||||||
|
|
||||||
var del_config: MenuItem? = null
|
|
||||||
var save_config: MenuItem? = null
|
|
||||||
|
|
||||||
private lateinit var configs: AngConfig
|
|
||||||
private var edit_index: Int = -1 //当前编辑的服务器
|
|
||||||
private var edit_guid: String = ""
|
|
||||||
private var isRunning: Boolean = false
|
|
||||||
private val securitys: Array<out String> by lazy {
|
private val securitys: Array<out String> by lazy {
|
||||||
resources.getStringArray(R.array.securitys)
|
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 {
|
private val networks: Array<out String> by lazy {
|
||||||
resources.getStringArray(R.array.networks)
|
resources.getStringArray(R.array.networks)
|
||||||
}
|
}
|
||||||
private val headertypes: Array<out String> by lazy {
|
private val tcpTypes: Array<out String> by lazy {
|
||||||
resources.getStringArray(R.array.headertypes)
|
resources.getStringArray(R.array.header_type_tcp)
|
||||||
}
|
}
|
||||||
private val streamsecuritys: Array<out String> by lazy {
|
private val kcpAndQuicTypes: Array<out String> by lazy {
|
||||||
resources.getStringArray(R.array.streamsecuritys)
|
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?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
setContentView(R.layout.activity_server)
|
|
||||||
|
|
||||||
configs = AngConfigManager.configs
|
|
||||||
edit_index = intent.getIntExtra("position", -1)
|
|
||||||
isRunning = intent.getBooleanExtra("isRunning", false)
|
|
||||||
title = getString(R.string.title_server)
|
title = getString(R.string.title_server)
|
||||||
|
|
||||||
if (edit_index >= 0) {
|
val config = MmkvManager.decodeServerConfig(editGuid)
|
||||||
edit_guid = configs.vmess[edit_index].guid
|
when(config?.configType ?: createConfigType) {
|
||||||
bindingServer(configs.vmess[edit_index])
|
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 {
|
} else {
|
||||||
clearServer()
|
clearServer()
|
||||||
}
|
}
|
||||||
@@ -58,33 +139,59 @@ class ServerActivity : BaseActivity() {
|
|||||||
/**
|
/**
|
||||||
* bingding seleced server config
|
* bingding seleced server config
|
||||||
*/
|
*/
|
||||||
fun bindingServer(vmess: AngConfig.VmessBean): Boolean {
|
private fun bindingServer(config: ServerConfig): Boolean {
|
||||||
et_remarks.text = Utils.getEditable(vmess.remarks)
|
val outbound = config.getProxyOutbound() ?: return false
|
||||||
|
val streamSetting = config.outboundBean?.streamSettings ?: return false
|
||||||
|
|
||||||
et_address.text = Utils.getEditable(vmess.address)
|
et_remarks.text = Utils.getEditable(config.remarks)
|
||||||
et_port.text = Utils.getEditable(vmess.port.toString())
|
et_address.text = Utils.getEditable(outbound.getServerAddress().orEmpty())
|
||||||
et_id.text = Utils.getEditable(vmess.id)
|
et_port.text = Utils.getEditable(outbound.getServerPort()?.toString() ?: DEFAULT_PORT.toString())
|
||||||
et_alterId.text = Utils.getEditable(vmess.alterId.toString())
|
et_id.text = Utils.getEditable(outbound.getPassword().orEmpty())
|
||||||
|
et_alterId?.text = Utils.getEditable(outbound.settings?.vnext?.get(0)?.users?.get(0)?.alterId.toString())
|
||||||
val security = Utils.arrayFind(securitys, vmess.security)
|
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) {
|
if (security >= 0) {
|
||||||
sp_security.setSelection(security)
|
sp_security?.setSelection(security)
|
||||||
}
|
|
||||||
val network = Utils.arrayFind(networks, vmess.network)
|
|
||||||
if (network >= 0) {
|
|
||||||
sp_network.setSelection(network)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val headerType = Utils.arrayFind(headertypes, vmess.headerType)
|
val streamSecurity = Utils.arrayFind(streamSecuritys, streamSetting.security)
|
||||||
if (headerType >= 0) {
|
|
||||||
sp_header_type.setSelection(headerType)
|
|
||||||
}
|
|
||||||
et_request_host.text = Utils.getEditable(vmess.requestHost)
|
|
||||||
et_path.text = Utils.getEditable(vmess.path)
|
|
||||||
|
|
||||||
val streamSecurity = Utils.arrayFind(streamsecuritys, vmess.streamSecurity)
|
|
||||||
if (streamSecurity >= 0) {
|
if (streamSecurity >= 0) {
|
||||||
sp_stream_security.setSelection(streamSecurity)
|
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
|
return true
|
||||||
}
|
}
|
||||||
@@ -92,113 +199,206 @@ class ServerActivity : BaseActivity() {
|
|||||||
/**
|
/**
|
||||||
* clear or init server config
|
* clear or init server config
|
||||||
*/
|
*/
|
||||||
fun clearServer(): Boolean {
|
private fun clearServer(): Boolean {
|
||||||
et_remarks.text = null
|
et_remarks.text = null
|
||||||
et_address.text = null
|
et_address.text = null
|
||||||
et_port.text = Utils.getEditable("10086")
|
et_port.text = Utils.getEditable(DEFAULT_PORT.toString())
|
||||||
et_id.text = null
|
et_id.text = null
|
||||||
et_alterId.text = Utils.getEditable("64")
|
et_alterId?.text = Utils.getEditable("0")
|
||||||
sp_security.setSelection(0)
|
sp_security?.setSelection(0)
|
||||||
sp_network.setSelection(0)
|
sp_network?.setSelection(0)
|
||||||
|
|
||||||
sp_header_type.setSelection(0)
|
sp_header_type?.setSelection(0)
|
||||||
et_request_host.text = null
|
et_request_host?.text = null
|
||||||
et_path.text = null
|
et_path?.text = null
|
||||||
sp_stream_security.setSelection(0)
|
sp_stream_security?.setSelection(0)
|
||||||
|
sp_allow_insecure?.setSelection(0)
|
||||||
|
et_sni?.text = null
|
||||||
|
|
||||||
|
//et_security.text = null
|
||||||
|
sp_flow?.setSelection(0)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* save server config
|
* save server config
|
||||||
*/
|
*/
|
||||||
fun saveServer(): Boolean {
|
private fun saveServer(): Boolean {
|
||||||
val vmess: AngConfig.VmessBean
|
if (TextUtils.isEmpty(et_remarks.text.toString())) {
|
||||||
if (edit_index >= 0) {
|
|
||||||
vmess = configs.vmess[edit_index]
|
|
||||||
} else {
|
|
||||||
vmess = AngConfig.VmessBean()
|
|
||||||
}
|
|
||||||
|
|
||||||
vmess.guid = edit_guid
|
|
||||||
vmess.remarks = et_remarks.text.toString()
|
|
||||||
vmess.address = et_address.text.toString()
|
|
||||||
vmess.port = Utils.parseInt(et_port.text.toString())
|
|
||||||
vmess.id = et_id.text.toString()
|
|
||||||
vmess.alterId = Utils.parseInt(et_alterId.text.toString())
|
|
||||||
vmess.security = securitys[sp_security.selectedItemPosition]
|
|
||||||
vmess.network = networks[sp_network.selectedItemPosition]
|
|
||||||
|
|
||||||
vmess.headerType = headertypes[sp_header_type.selectedItemPosition]
|
|
||||||
vmess.requestHost = et_request_host.text.toString()
|
|
||||||
vmess.path = et_path.text.toString()
|
|
||||||
vmess.streamSecurity = streamsecuritys[sp_stream_security.selectedItemPosition]
|
|
||||||
|
|
||||||
if (TextUtils.isEmpty(vmess.remarks)) {
|
|
||||||
toast(R.string.server_lab_remarks)
|
toast(R.string.server_lab_remarks)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if (TextUtils.isEmpty(vmess.address)) {
|
if (TextUtils.isEmpty(et_address.text.toString())) {
|
||||||
toast(R.string.server_lab_address)
|
toast(R.string.server_lab_address)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if (TextUtils.isEmpty(vmess.port.toString()) || vmess.port <= 0) {
|
val port = Utils.parseInt(et_port.text.toString())
|
||||||
|
if (port <= 0) {
|
||||||
toast(R.string.server_lab_port)
|
toast(R.string.server_lab_port)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if (TextUtils.isEmpty(vmess.id)) {
|
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)
|
toast(R.string.server_lab_id)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if (TextUtils.isEmpty(vmess.alterId.toString()) || vmess.alterId < 0) {
|
sp_stream_security?.let {
|
||||||
toast(R.string.server_lab_alterid)
|
if (config.configType == EConfigType.TROJAN && TextUtils.isEmpty(streamSecuritys[it.selectedItemPosition])) {
|
||||||
return false
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (AngConfigManager.addServer(vmess, edit_index) == 0) {
|
config.remarks = et_remarks.text.toString().trim()
|
||||||
toast(R.string.toast_success)
|
config.outboundBean?.settings?.vnext?.get(0)?.let { vnext ->
|
||||||
finish()
|
saveVnext(vnext, port, config)
|
||||||
return true
|
}
|
||||||
|
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()
|
||||||
|
if (streamSecuritys[sp_stream_security?.selectedItemPosition ?: 0] == XTLS) {
|
||||||
|
vnext.users[0].flow = flows[sp_flow?.selectedItemPosition ?: 0].ifBlank { DEFAULT_FLOW }
|
||||||
|
} else {
|
||||||
|
vnext.users[0].flow = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun saveServers(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] == XTLS) {
|
||||||
|
flows[sp_flow?.selectedItemPosition ?: 0].ifBlank { DEFAULT_FLOW }
|
||||||
|
} 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 {
|
} else {
|
||||||
toast(R.string.toast_failure)
|
allowinsecures[allowInsecureField].toBoolean()
|
||||||
return false
|
}
|
||||||
|
|
||||||
|
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
|
* save server config
|
||||||
*/
|
*/
|
||||||
fun deleteServer(): Boolean {
|
private fun deleteServer(): Boolean {
|
||||||
if (edit_index >= 0) {
|
if (editGuid.isNotEmpty()) {
|
||||||
alert(R.string.del_config_comfirm) {
|
if (editGuid != mainStorage?.decodeString(MmkvManager.KEY_SELECTED_SERVER)) {
|
||||||
positiveButton(android.R.string.ok) {
|
if (settingsStorage?.decodeBool(AppConfig.PREF_CONFIRM_REMOVE) == true) {
|
||||||
if (AngConfigManager.removeServer(edit_index) == 0) {
|
AlertDialog.Builder(this).setMessage(R.string.del_config_comfirm)
|
||||||
toast(R.string.toast_success)
|
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||||
finish()
|
MmkvManager.removeServer(editGuid)
|
||||||
} else {
|
finish()
|
||||||
toast(R.string.toast_failure)
|
}
|
||||||
}
|
.show()
|
||||||
|
} else {
|
||||||
|
MmkvManager.removeServer(editGuid)
|
||||||
|
finish()
|
||||||
}
|
}
|
||||||
show()
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
|
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||||
menuInflater.inflate(R.menu.action_server, menu)
|
menuInflater.inflate(R.menu.action_server, menu)
|
||||||
del_config = menu?.findItem(R.id.del_config)
|
val delButton = menu.findItem(R.id.del_config)
|
||||||
save_config = menu?.findItem(R.id.save_config)
|
val saveButton = menu.findItem(R.id.save_config)
|
||||||
|
|
||||||
if (edit_index >= 0) {
|
if (editGuid.isNotEmpty()) {
|
||||||
if (isRunning) {
|
if (isRunning) {
|
||||||
if (edit_index == configs.index) {
|
delButton?.isVisible = false
|
||||||
del_config?.isVisible = false
|
saveButton?.isVisible = false
|
||||||
save_config?.isVisible = false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
del_config?.isVisible = false
|
delButton?.isVisible = false
|
||||||
}
|
}
|
||||||
|
|
||||||
return super.onCreateOptionsMenu(menu)
|
return super.onCreateOptionsMenu(menu)
|
||||||
@@ -215,4 +415,4 @@ class ServerActivity : BaseActivity() {
|
|||||||
}
|
}
|
||||||
else -> super.onOptionsItemSelected(item)
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,51 +1,18 @@
|
|||||||
package com.v2ray.ang.ui
|
package com.v2ray.ang.ui
|
||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.SharedPreferences
|
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.preference.*
|
import android.text.TextUtils
|
||||||
import com.v2ray.ang.AngApplication
|
import android.view.View
|
||||||
import com.v2ray.ang.BuildConfig
|
import androidx.activity.viewModels
|
||||||
//import com.v2ray.ang.InappBuyActivity
|
import androidx.preference.*
|
||||||
import com.v2ray.ang.R
|
|
||||||
import com.v2ray.ang.AppConfig
|
import com.v2ray.ang.AppConfig
|
||||||
import com.v2ray.ang.extension.defaultDPreference
|
import com.v2ray.ang.R
|
||||||
import com.v2ray.ang.extension.onClick
|
|
||||||
import com.v2ray.ang.util.Utils
|
import com.v2ray.ang.util.Utils
|
||||||
import org.jetbrains.anko.act
|
import com.v2ray.ang.viewmodel.SettingsViewModel
|
||||||
import org.jetbrains.anko.defaultSharedPreferences
|
|
||||||
import org.jetbrains.anko.startActivity
|
|
||||||
import org.jetbrains.anko.toast
|
|
||||||
import libv2ray.Libv2ray
|
|
||||||
|
|
||||||
class SettingsActivity : BaseActivity() {
|
class SettingsActivity : BaseActivity() {
|
||||||
companion object {
|
private val settingsViewModel: SettingsViewModel by viewModels()
|
||||||
// const val PREF_BYPASS_MAINLAND = "pref_bypass_mainland"
|
|
||||||
// const val PREF_START_ON_BOOT = "pref_start_on_boot"
|
|
||||||
const val PREF_PER_APP_PROXY = "pref_per_app_proxy"
|
|
||||||
// const val PREF_MUX_ENAimport libv2ray.Libv2rayBLED = "pref_mux_enabled"
|
|
||||||
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_REMOTE_DNS = "pref_remote_dns"
|
|
||||||
const val PREF_DOMESTIC_DNS = "pref_domestic_dns"
|
|
||||||
|
|
||||||
// const val PREF_SOCKS_PORT = "pref_socks_port"
|
|
||||||
// const val PREF_HTTP_PORT = "pref_http_port"
|
|
||||||
|
|
||||||
const val PREF_ROUTING_DOMAIN_STRATEGY = "pref_routing_domain_strategy"
|
|
||||||
const val PREF_ROUTING_MODE = "pref_routing_mode"
|
|
||||||
const val PREF_ROUTING_CUSTOM = "pref_routing_custom"
|
|
||||||
// const val PREF_DONATE = "pref_donate"
|
|
||||||
// const val PREF_LICENSES = "pref_licenses"
|
|
||||||
// const val PREF_FEEDBACK = "pref_feedback"
|
|
||||||
// const val PREF_TG_GROUP = "pref_tg_group"
|
|
||||||
const val PREF_VERSION = "pref_version"
|
|
||||||
// const val PREF_AUTO_RESTART = "pref_auto_restart"
|
|
||||||
const val PREF_FORWARD_IPV6 = "pref_forward_ipv6"
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
@@ -54,126 +21,35 @@ class SettingsActivity : BaseActivity() {
|
|||||||
title = getString(R.string.title_settings)
|
title = getString(R.string.title_settings)
|
||||||
|
|
||||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||||
|
|
||||||
|
settingsViewModel.startListenPreferenceChange()
|
||||||
}
|
}
|
||||||
|
|
||||||
class SettingsFragment : PreferenceFragment(), SharedPreferences.OnSharedPreferenceChangeListener {
|
class SettingsFragment : PreferenceFragmentCompat() {
|
||||||
val perAppProxy by lazy { findPreference(PREF_PER_APP_PROXY) as CheckBoxPreference }
|
private val perAppProxy by lazy { findPreference<CheckBoxPreference>(AppConfig.PREF_PER_APP_PROXY) }
|
||||||
val sppedEnabled by lazy { findPreference(PREF_SPEED_ENABLED) as CheckBoxPreference }
|
private val localDns by lazy { findPreference<CheckBoxPreference>(AppConfig.PREF_LOCAL_DNS_ENABLED) }
|
||||||
val sniffingEnabled by lazy { findPreference(PREF_SNIFFING_ENABLED) as CheckBoxPreference }
|
private val fakeDns by lazy { findPreference<CheckBoxPreference>(AppConfig.PREF_FAKE_DNS_ENABLED) }
|
||||||
val proxySharing by lazy { findPreference(PREF_PROXY_SHARING) as CheckBoxPreference }
|
private val localDnsPort by lazy { findPreference<EditTextPreference>(AppConfig.PREF_LOCAL_DNS_PORT) }
|
||||||
val domainStrategy by lazy { findPreference(PREF_ROUTING_DOMAIN_STRATEGY) as ListPreference }
|
private val vpnDns by lazy { findPreference<EditTextPreference>(AppConfig.PREF_VPN_DNS) }
|
||||||
val routingMode by lazy { findPreference(PREF_ROUTING_MODE) as ListPreference }
|
|
||||||
|
|
||||||
val forwardIpv6 by lazy { findPreference(PREF_FORWARD_IPV6) as CheckBoxPreference }
|
|
||||||
val enableLocalDns by lazy { findPreference(PREF_LOCAL_DNS_ENABLED) as CheckBoxPreference }
|
|
||||||
val domesticDns by lazy { findPreference(PREF_DOMESTIC_DNS) as EditTextPreference }
|
|
||||||
val remoteDns by lazy { findPreference(PREF_REMOTE_DNS) as EditTextPreference }
|
|
||||||
|
|
||||||
// val autoRestart by lazy { findPreference(PREF_AUTO_RESTART) as CheckBoxPreference }
|
// 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) }
|
||||||
// val socksPort by lazy { findPreference(PREF_SOCKS_PORT) as EditTextPreference }
|
private val socksPort by lazy { findPreference<EditTextPreference>(AppConfig.PREF_SOCKS_PORT) }
|
||||||
// val httpPort by lazy { findPreference(PREF_HTTP_PORT) as EditTextPreference }
|
private val httpPort by lazy { findPreference<EditTextPreference>(AppConfig.PREF_HTTP_PORT) }
|
||||||
|
private val routingCustom by lazy { findPreference<Preference>(AppConfig.PREF_ROUTING_CUSTOM) }
|
||||||
val routingCustom: Preference by lazy { findPreference(PREF_ROUTING_CUSTOM) }
|
|
||||||
// val donate: Preference by lazy { findPreference(PREF_DONATE) }
|
|
||||||
// val licenses: Preference by lazy { findPreference(PREF_LICENSES) }
|
// val licenses: Preference by lazy { findPreference(PREF_LICENSES) }
|
||||||
// val feedback: Preference by lazy { findPreference(PREF_FEEDBACK) }
|
// val feedback: Preference by lazy { findPreference(PREF_FEEDBACK) }
|
||||||
// val tgGroup: Preference by lazy { findPreference(PREF_TG_GROUP) }
|
// val tgGroup: Preference by lazy { findPreference(PREF_TG_GROUP) }
|
||||||
val version: Preference by lazy { findPreference(PREF_VERSION) }
|
|
||||||
|
|
||||||
private fun restartProxy() {
|
private val mode by lazy { findPreference<ListPreference>(AppConfig.PREF_MODE) }
|
||||||
Utils.stopVService(activity)
|
|
||||||
Utils.startVService(activity)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun isRunning(): Boolean {
|
override fun onCreatePreferences(bundle: Bundle?, s: String?) {
|
||||||
return Utils.isServiceRun(activity, "com.v2ray.ang.service.V2RayVpnService")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
addPreferencesFromResource(R.xml.pref_settings)
|
addPreferencesFromResource(R.xml.pref_settings)
|
||||||
var app = activity.application as AngApplication
|
|
||||||
|
|
||||||
perAppProxy.setOnPreferenceClickListener {
|
routingCustom?.setOnPreferenceClickListener {
|
||||||
if (isRunning()) {
|
startActivity(Intent(activity, RoutingSettingsActivity::class.java))
|
||||||
Utils.stopVService(activity)
|
false
|
||||||
}
|
|
||||||
startActivity<PerAppProxyActivity>()
|
|
||||||
perAppProxy.isChecked = true
|
|
||||||
true
|
|
||||||
}
|
}
|
||||||
sppedEnabled.setOnPreferenceClickListener {
|
|
||||||
if (isRunning())
|
|
||||||
restartProxy()
|
|
||||||
true
|
|
||||||
}
|
|
||||||
sniffingEnabled.setOnPreferenceClickListener {
|
|
||||||
if (isRunning())
|
|
||||||
restartProxy()
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
proxySharing.setOnPreferenceClickListener {
|
|
||||||
if (proxySharing.isChecked)
|
|
||||||
toast(R.string.toast_warning_pref_proxysharing)
|
|
||||||
if (isRunning())
|
|
||||||
restartProxy()
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
domainStrategy.setOnPreferenceChangeListener { _, _ ->
|
|
||||||
if (isRunning())
|
|
||||||
restartProxy()
|
|
||||||
true
|
|
||||||
}
|
|
||||||
routingMode.setOnPreferenceChangeListener { _, _ ->
|
|
||||||
if (isRunning())
|
|
||||||
restartProxy()
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
routingCustom.onClick {
|
|
||||||
if (isRunning())
|
|
||||||
Utils.stopVService(activity)
|
|
||||||
startActivity<RoutingSettingsActivity>()
|
|
||||||
}
|
|
||||||
|
|
||||||
forwardIpv6.setOnPreferenceClickListener {
|
|
||||||
if (isRunning())
|
|
||||||
restartProxy()
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
enableLocalDns.setOnPreferenceClickListener {
|
|
||||||
if (isRunning())
|
|
||||||
restartProxy()
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
domesticDns.setOnPreferenceChangeListener { preference, any ->
|
|
||||||
// domesticDns.summary = any as String
|
|
||||||
val nval = any as String
|
|
||||||
domesticDns.summary = if (nval == "") AppConfig.DNS_DIRECT else nval
|
|
||||||
if (isRunning())
|
|
||||||
restartProxy()
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
remoteDns.setOnPreferenceChangeListener { preference, any ->
|
|
||||||
// remoteDns.summary = any as String
|
|
||||||
val nval = any as String
|
|
||||||
remoteDns.summary = if (nval == "") AppConfig.DNS_AGENT else nval
|
|
||||||
if (isRunning())
|
|
||||||
restartProxy()
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
// donate.onClick {
|
|
||||||
// startActivity<InappBuyActivity>()
|
|
||||||
// }
|
|
||||||
|
|
||||||
// licenses.onClick {
|
// licenses.onClick {
|
||||||
// val fragment = LicensesDialogFragment.Builder(act)
|
// val fragment = LicensesDialogFragment.Builder(act)
|
||||||
@@ -197,54 +73,110 @@ class SettingsActivity : BaseActivity() {
|
|||||||
// }
|
// }
|
||||||
// }
|
// }
|
||||||
|
|
||||||
|
perAppProxy?.setOnPreferenceClickListener {
|
||||||
|
startActivity(Intent(activity, PerAppProxyActivity::class.java))
|
||||||
|
perAppProxy?.isChecked = true
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
// socksPort.setOnPreferenceChangeListener { preference, any ->
|
remoteDns?.setOnPreferenceChangeListener { _, any ->
|
||||||
// socksPort.summary = any as String
|
// remoteDns.summary = any as String
|
||||||
// true
|
val nval = any as String
|
||||||
// }
|
remoteDns?.summary = if (nval == "") AppConfig.DNS_AGENT else nval
|
||||||
// httpPort.setOnPreferenceChangeListener { preference, any ->
|
true
|
||||||
// httpPort.summary = any as String
|
}
|
||||||
// true
|
domesticDns?.setOnPreferenceChangeListener { _, any ->
|
||||||
// }
|
// domesticDns.summary = any as String
|
||||||
|
val nval = any as String
|
||||||
|
domesticDns?.summary = if (nval == "") AppConfig.DNS_DIRECT else nval
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
version.summary = "${BuildConfig.VERSION_NAME} (${Libv2ray.checkVersionX()})"
|
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() {
|
override fun onStart() {
|
||||||
super.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, "")
|
||||||
|
|
||||||
perAppProxy.isChecked = defaultSharedPreferences.getBoolean(PREF_PER_APP_PROXY, false)
|
localDnsPort?.summary = defaultSharedPreferences.getString(AppConfig.PREF_LOCAL_DNS_PORT, AppConfig.PORT_LOCAL_DNS)
|
||||||
remoteDns.summary = defaultSharedPreferences.getString(PREF_REMOTE_DNS, "")
|
socksPort?.summary = defaultSharedPreferences.getString(AppConfig.PREF_SOCKS_PORT, AppConfig.PORT_SOCKS)
|
||||||
domesticDns.summary = defaultSharedPreferences.getString(PREF_DOMESTIC_DNS, "")
|
httpPort?.summary = defaultSharedPreferences.getString(AppConfig.PREF_HTTP_PORT, AppConfig.PORT_HTTP)
|
||||||
|
|
||||||
if (remoteDns.summary == "") {
|
if (TextUtils.isEmpty(remoteDnsString)) {
|
||||||
remoteDns.summary = AppConfig.DNS_AGENT
|
remoteDnsString = AppConfig.DNS_AGENT
|
||||||
}
|
}
|
||||||
|
if (TextUtils.isEmpty(domesticDns?.summary)) {
|
||||||
if ( domesticDns.summary == "") {
|
domesticDns?.summary = AppConfig.DNS_DIRECT
|
||||||
domesticDns.summary = AppConfig.DNS_DIRECT
|
|
||||||
}
|
}
|
||||||
|
remoteDns?.summary = remoteDnsString
|
||||||
|
vpnDns?.summary = defaultSharedPreferences.getString(AppConfig.PREF_VPN_DNS, remoteDnsString)
|
||||||
|
|
||||||
// socksPort.summary = defaultSharedPreferences.getString(PREF_SOCKS_PORT, "10808")
|
if (TextUtils.isEmpty(localDnsPort?.summary)) {
|
||||||
// lanconnPort.summary = defaultSharedPreferences.getString(PREF_HTTP_PORT, "")
|
localDnsPort?.summary = AppConfig.PORT_LOCAL_DNS
|
||||||
|
}
|
||||||
defaultSharedPreferences.registerOnSharedPreferenceChangeListener(this)
|
if (TextUtils.isEmpty(socksPort?.summary)) {
|
||||||
|
socksPort?.summary = AppConfig.PORT_SOCKS
|
||||||
|
}
|
||||||
|
if (TextUtils.isEmpty(httpPort?.summary)) {
|
||||||
|
httpPort?.summary = AppConfig.PORT_HTTP
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStop() {
|
private fun updateMode(mode: String?) {
|
||||||
super.onStop()
|
val defaultSharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireActivity())
|
||||||
defaultSharedPreferences.unregisterOnSharedPreferenceChangeListener(this)
|
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))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String?) {
|
private fun updateLocalDns(enabled: Boolean) {
|
||||||
when (key) {
|
fakeDns?.isEnabled = enabled
|
||||||
// PREF_AUTO_RESTART ->
|
localDnsPort?.isEnabled = enabled
|
||||||
// act.defaultDPreference.setPrefBoolean(key, sharedPreferences.getBoolean(key, false))
|
vpnDns?.isEnabled = !enabled
|
||||||
|
|
||||||
PREF_PER_APP_PROXY ->
|
|
||||||
act.defaultDPreference.setPrefBoolean(key, sharedPreferences.getBoolean(key, false))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
fun onModeHelpClicked(view: View) {
|
||||||
|
Utils.openUri(this, AppConfig.v2rayNGWikiMode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user