Compare commits
310 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 | ||
|
|
76f52e7aa7 | ||
|
|
caa25ce424 | ||
|
|
e33d7e9bcf | ||
|
|
e120fce0b7 |
@@ -1,3 +1,8 @@
|
||||
---
|
||||
name: v2rayNG程序问题
|
||||
about: 创建一个报告来帮助我们改进
|
||||
---
|
||||
|
||||
在提出问题前请先自行排除服务器端问题,同时也请通过搜索确认是否有人提出过相同问题。
|
||||
|
||||
|
||||
@@ -14,7 +19,8 @@
|
||||
|
||||
### 日志信息
|
||||
<details>
|
||||
通过 `adb logcat -s com.v2ray.ang GoLog V2rayConfigUtilGoLog Main` 获取日志。请自行删减日志中可能出现的敏感信息。
|
||||
|
||||
通过`adb logcat -s com.v2ray.ang GoLog V2rayConfigUtilGoLog Main`获取日志。请自行删减日志中可能出现的敏感信息。
|
||||
|
||||
如果问题可重现,建议先执行`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的问题,请至这个链接讨论。
|
||||
10
.gitignore
vendored
10
.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
|
||||
*.jks
|
||||
V2rayNG/gradle/wrapper/gradle-wrapper.properties
|
||||
V2rayNG/gradle/wrapper/gradle-wrapper.properties
|
||||
V2rayNG/app/release/output.json
|
||||
.idea/
|
||||
.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
|
||||
|
||||
### 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
|
||||
|
||||
A V2Ray client for Android, support [Xray core](https://github.com/XTLS/Xray-core) and [v2fly core](https://github.com/v2fly/v2ray-core)
|
||||
|
||||
[](https://developer.android.com/about/versions/lollipop)
|
||||
[](https://kotlinlang.org)
|
||||
[](https://github.com/2dust/v2rayNG/commits/master)
|
||||
[](https://www.codefactor.io/repository/github/2dust/v2rayng)
|
||||
[](https://github.com/2dust/v2rayNG/releases)
|
||||
[](https://t.me/v2rayn)
|
||||
|
||||
<a href="https://play.google.com/store/apps/details?id=com.v2ray.ang">
|
||||
<img alt="Get it on Google Play" src="https://play.google.com/intl/en_us/badges/images/generic/en_badge_web_generic.png" width="165" height="64" />
|
||||
</a>
|
||||
|
||||
### Telegram Channel
|
||||
[github_2dust](https://t.me/github_2dust)
|
||||
|
||||
### Usage
|
||||
|
||||
#### Geoip and Geosite
|
||||
- geoip.dat and geosite.dat files are in `Android/data/com.v2ray.ang/files/assets` (path may differ on some Android device)
|
||||
- download feature will get enhanced version in this [repo](https://github.com/Loyalsoldier/v2ray-rules-dat) (Note it need a working proxy)
|
||||
- latest official [domain list](https://github.com/v2fly/domain-list-community) and [ip list](https://github.com/v2fly/geoip) can be imported manually
|
||||
- possible to use third party dat file in the same folder, like [h2y](https://guide.v2fly.org/routing/sitedata.html#%E5%A4%96%E7%BD%AE%E7%9A%84%E5%9F%9F%E5%90%8D%E6%96%87%E4%BB%B6)
|
||||
|
||||
### More in our [wiki](https://github.com/2dust/v2rayNG/wiki)
|
||||
|
||||
### Development guide
|
||||
|
||||
Android project under V2rayNG folder can be compiled directly in Android Studio, or using Gradle wrapper. But the v2ray core inside the aar is (probably) outdated.
|
||||
The aar can be compiled from the Golang project [AndroidLibV2rayLite](https://github.com/2dust/AndroidLibV2rayLite) or [AndroidLibXrayLite](https://github.com/2dust/AndroidLibXrayLite).
|
||||
For a quick start, read guide for [Go Mobile](https://github.com/golang/go/wiki/Mobile) and [Makefiles for Go Developers](https://tutorialedge.net/golang/makefiles-for-go-developers/)
|
||||
|
||||
v2rayNG can run on Android Emulators. For WSA, VPN permission need to be granted via
|
||||
`appops set [package name] ACTIVATE_VPN allow`
|
||||
|
||||
1
V2rayNG/.gitignore
vendored
1
V2rayNG/.gitignore
vendored
@@ -7,3 +7,4 @@
|
||||
/captures
|
||||
*.apk
|
||||
signing.properties
|
||||
*.aar
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'kotlin-android-extensions'
|
||||
|
||||
Properties props = new Properties()
|
||||
props.load(new FileInputStream(new File('local.properties')))
|
||||
|
||||
android {
|
||||
compileSdkVersion 28
|
||||
buildToolsVersion '28.0.3'
|
||||
compileSdkVersion Integer.parseInt("$compileSdkVer")
|
||||
buildToolsVersion "$buildToolsVer"
|
||||
|
||||
compileOptions {
|
||||
targetCompatibility = "8"
|
||||
@@ -13,13 +15,14 @@ android {
|
||||
|
||||
defaultConfig {
|
||||
applicationId "com.v2ray.ang"
|
||||
minSdkVersion 17
|
||||
minSdkVersion 21
|
||||
targetSdkVersion Integer.parseInt("$targetSdkVer")
|
||||
multiDexEnabled true
|
||||
versionCode 212
|
||||
versionName "1.0.2"
|
||||
versionCode 478
|
||||
versionName "1.7.22"
|
||||
}
|
||||
|
||||
if (props["sign"]) {
|
||||
signingConfigs {
|
||||
release {
|
||||
storeFile file("../key.jks")
|
||||
@@ -34,25 +37,39 @@ android {
|
||||
storePassword '123456'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
zipAlignEnabled false
|
||||
shrinkResources false
|
||||
if (props["sign"]) {
|
||||
signingConfig signingConfigs.release
|
||||
}
|
||||
ndk.abiFilters 'x86', 'x86_64', 'armeabi-v7a', 'arm64-v8a'
|
||||
// proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
debug {
|
||||
minifyEnabled false
|
||||
zipAlignEnabled false
|
||||
shrinkResources false
|
||||
if (props["sign"]) {
|
||||
signingConfig signingConfigs.release
|
||||
}
|
||||
ndk.abiFilters 'x86', 'x86_64', 'armeabi-v7a', 'arm64-v8a'
|
||||
}
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
main.java.srcDirs += 'src/main/kotlin'
|
||||
main {
|
||||
jniLibs.srcDirs = ['libs']
|
||||
java.srcDirs += 'src/main/kotlin'
|
||||
}
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
splits {
|
||||
@@ -70,60 +87,65 @@ android {
|
||||
android.applicationVariants.all { variant ->
|
||||
// assign different version code for each output
|
||||
variant.outputs.each { output ->
|
||||
output.outputFileName = "v2rayNG_" + variant.versionName + "_" + output.getFilter(com.android.build.OutputFile.ABI) + ".apk"
|
||||
|
||||
output.versionCodeOverride =
|
||||
project.ext.versionCodes.get(output.getFilter(com.android.build.OutputFile.ABI), 0) *
|
||||
1000000 + android.defaultConfig.versionCode
|
||||
}
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
viewBinding true
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation fileTree(include: ['*.jar'], dir: 'libs')
|
||||
implementation 'com.android.support.constraint:constraint-layout:1.1.3'
|
||||
testImplementation 'junit:junit:4.12'
|
||||
implementation project(':dpreference')
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion"
|
||||
implementation fileTree(dir: 'libs', include: ['*.aar', '*.jar'], exclude: [])
|
||||
testImplementation 'junit:junit:4.13.2'
|
||||
|
||||
// Androidx
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
||||
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
|
||||
implementation 'androidx.appcompat:appcompat:1.4.2'
|
||||
implementation 'com.google.android.material:material:1.6.1'
|
||||
implementation 'androidx.cardview:cardview:1.0.0'
|
||||
implementation 'androidx.preference:preference-ktx:1.2.0'
|
||||
implementation 'androidx.recyclerview:recyclerview:1.2.1'
|
||||
implementation 'androidx.fragment:fragment-ktx:1.5.2'
|
||||
implementation 'androidx.multidex:multidex:2.0.1'
|
||||
implementation 'androidx.viewpager2:viewpager2:1.1.0-beta01'
|
||||
|
||||
// Androidx ktx
|
||||
implementation 'androidx.activity:activity-ktx:1.5.1'
|
||||
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1'
|
||||
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.5.1'
|
||||
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.5.1'
|
||||
|
||||
//kotlin
|
||||
implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlinVersion"
|
||||
// Android support library
|
||||
implementation "com.android.support:support-v4:$supportLibVersion"
|
||||
implementation "com.android.support:appcompat-v7:$supportLibVersion"
|
||||
implementation "com.android.support:design:$supportLibVersion"
|
||||
implementation "com.android.support:cardview-v7:$supportLibVersion"
|
||||
implementation "com.android.support:preference-v7:$supportLibVersion"
|
||||
implementation "com.android.support:recyclerview-v7:$supportLibVersion"
|
||||
// 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 "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2"
|
||||
|
||||
implementation 'com.tencent:mmkv-static:1.2.12'
|
||||
implementation 'com.google.code.gson:gson:2.8.9'
|
||||
implementation 'io.reactivex:rxjava:1.3.4'
|
||||
implementation 'io.reactivex:rxandroid:1.2.1'
|
||||
implementation 'com.tbruyelle.rxpermissions:rxpermissions:0.9.4@aar'
|
||||
implementation '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:zxing:1.9.8'
|
||||
implementation 'com.github.jorgecastilloprz:fabprogresscircle:1.01@aar'
|
||||
implementation 'com.beust:klaxon:3.0.1'
|
||||
implementation 'com.android.support:multidex:1.0.3'
|
||||
|
||||
implementation(name: 'libv2ray', ext: 'aar')
|
||||
//implementation(name: 'tun2socks', ext: 'aar')
|
||||
implementation 'me.drakeet.support:toastcompat:1.1.0'
|
||||
implementation 'com.blacksquircle.ui:editorkit:2.1.1'
|
||||
implementation 'com.blacksquircle.ui:language-base:2.1.1'
|
||||
implementation 'com.blacksquircle.ui:language-json:2.1.1'
|
||||
}
|
||||
|
||||
buildscript {
|
||||
repositories {
|
||||
google()
|
||||
jcenter()
|
||||
maven { url 'https://maven.google.com' }
|
||||
}
|
||||
dependencies {
|
||||
classpath "org.jetbrains.kotlin:kotlin-android-extensions:$kotlinVersion"
|
||||
}
|
||||
}
|
||||
repositories {
|
||||
flatDir {
|
||||
dirs 'libs'
|
||||
}
|
||||
}
|
||||
//buildscript {
|
||||
// repositories {
|
||||
// google()
|
||||
// mavenCentral()
|
||||
// maven { url 'https://maven.google.com' }
|
||||
// maven { url 'https://jitpack.io' }
|
||||
// }
|
||||
//}
|
||||
|
||||
@@ -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"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="com.v2ray.ang">
|
||||
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
|
||||
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE"/>
|
||||
<supports-screens
|
||||
android:anyDensity="true"
|
||||
android:smallScreens="true"
|
||||
android:normalScreens="true"
|
||||
android:largeScreens="true"
|
||||
android:xlargeScreens="true"/>
|
||||
|
||||
<uses-feature android:name="android.hardware.camera" android:required="false"/>
|
||||
<uses-feature android:name="android.hardware.camera.autofocus" android:required="false"/>
|
||||
|
||||
<!-- https://developer.android.com/about/versions/11/privacy/package-visibility -->
|
||||
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"
|
||||
tools:ignore="QueryAllPackagesPermission" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||
<!-- <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="com.android.vending.BILLING" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<!-- <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> -->
|
||||
|
||||
<application
|
||||
android:name=".AngApplication"
|
||||
android:allowBackup="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/AppTheme">
|
||||
android:theme="@style/AppThemeLight"
|
||||
android:usesCleartextTraffic="true"
|
||||
tools:targetApi="m">
|
||||
<activity
|
||||
android:exported="true"
|
||||
android:name=".ui.MainActivity"
|
||||
android:theme="@style/AppTheme.NoActionBar"
|
||||
android:launchMode="singleTask">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
|
||||
<data android:mimeType="text/plain" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.service.quicksettings.action.QS_TILE_PREFERENCES" />
|
||||
</intent-filter>
|
||||
|
||||
<meta-data
|
||||
android:name="android.app.shortcuts"
|
||||
android:resource="@xml/shortcuts" />
|
||||
</activity>
|
||||
<activity
|
||||
android:exported="false"
|
||||
android:name=".ui.ServerActivity"
|
||||
android:windowSoftInputMode="stateUnchanged" />
|
||||
<activity
|
||||
android:name=".ui.Server2Activity"
|
||||
android:exported="false"
|
||||
android:name=".ui.ServerCustomConfigActivity"
|
||||
android:windowSoftInputMode="stateUnchanged" />
|
||||
<activity
|
||||
android:name=".ui.Server3Activity"
|
||||
android:windowSoftInputMode="stateUnchanged" />
|
||||
android:exported="false"
|
||||
android:name=".ui.SettingsActivity" />
|
||||
<activity
|
||||
android:name=".ui.Server4Activity"
|
||||
android:windowSoftInputMode="stateUnchanged" />
|
||||
<activity android:name=".ui.SettingsActivity" />
|
||||
<activity android:name=".ui.PerAppProxyActivity" />
|
||||
<activity android:name=".ui.ScannerActivity" />
|
||||
<!-- <activity android:name=".InappBuyActivity" />-->
|
||||
<activity android:name=".ui.LogcatActivity" />
|
||||
android:exported="false"
|
||||
android:name=".ui.PerAppProxyActivity" />
|
||||
<activity
|
||||
android:exported="false"
|
||||
android:name=".ui.ScannerActivity" />
|
||||
<activity
|
||||
android:exported="false"
|
||||
android:name=".ui.LogcatActivity" />
|
||||
<activity
|
||||
android:exported="false"
|
||||
android:name=".ui.RoutingSettingsActivity"
|
||||
android:windowSoftInputMode="stateUnchanged" />
|
||||
<activity android: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 android:name=".ui.ScScannerActivity" />
|
||||
<activity android:name=".ui.ScSwitchActivity" />
|
||||
<activity
|
||||
android:exported="false"
|
||||
android:name=".ui.SubEditActivity" />
|
||||
<activity
|
||||
android:exported="false"
|
||||
android:name=".ui.ScScannerActivity" />
|
||||
<activity
|
||||
android:exported="false"
|
||||
android:name=".ui.ScSwitchActivity"
|
||||
android:excludeFromRecents="true"
|
||||
android:process=":RunSoLibV2RayDaemon"
|
||||
android:theme="@style/AppTheme.NoActionBar.Translucent" />
|
||||
|
||||
<activity
|
||||
android:exported="true"
|
||||
android:name=".ui.UrlSchemeActivity">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:mimeType="text/plain" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
<category android:name="android.intent.category.BROWSABLE"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<data android:scheme="v2rayng"
|
||||
android:host="install-config" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<service
|
||||
android:name=".service.V2RayVpnService"
|
||||
@@ -83,28 +125,45 @@
|
||||
android:value="true" />
|
||||
</service>
|
||||
|
||||
<!--<receiver android:name=".receiver.WidgetProvider">-->
|
||||
<!--<meta-data-->
|
||||
<!--android:name="android.appwidget.provider"-->
|
||||
<!--android:resource="@xml/app_widget_provider" />-->
|
||||
<service android:name=".service.V2RayProxyOnlyService"
|
||||
android:exported="false"
|
||||
android:label="@string/app_name"
|
||||
android:process=":RunSoLibV2RayDaemon">
|
||||
</service>
|
||||
|
||||
<!--<intent-filter>-->
|
||||
<!--<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />-->
|
||||
<!--<action android:name="com.v2ray.ang.action.widget.click" />-->
|
||||
<!--</intent-filter>-->
|
||||
<!--</receiver>-->
|
||||
<service android:name=".service.V2RayTestService"
|
||||
android:exported="false"
|
||||
android:process=":RunSoLibV2RayDaemon">
|
||||
</service>
|
||||
|
||||
<receiver
|
||||
android:exported="true"
|
||||
android:name=".receiver.WidgetProvider"
|
||||
android:process=":RunSoLibV2RayDaemon">
|
||||
<meta-data
|
||||
android:name="android.appwidget.provider"
|
||||
android:resource="@xml/app_widget_provider" />
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
||||
<action android:name="com.v2ray.ang.action.widget.click" />
|
||||
<action android:name="com.v2ray.ang.action.activity" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<service
|
||||
android:exported="true"
|
||||
android:name=".service.QSTileService"
|
||||
android:icon="@drawable/ic_v"
|
||||
android:icon="@drawable/ic_stat_name"
|
||||
android:label="@string/app_tile_name"
|
||||
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
|
||||
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE"
|
||||
android:process=":RunSoLibV2RayDaemon">
|
||||
<intent-filter>
|
||||
<action android:name="android.service.quicksettings.action.QS_TILE" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
<!-- =====================Tasker===================== -->
|
||||
<activity
|
||||
android:exported="true"
|
||||
android:name=".ui.TaskerActivity"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name">
|
||||
@@ -113,7 +172,10 @@
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<receiver android:name=".receiver.TaskerReceiver">
|
||||
<receiver
|
||||
android:exported="true"
|
||||
android:name=".receiver.TaskerReceiver"
|
||||
android:process=":RunSoLibV2RayDaemon">
|
||||
<intent-filter>
|
||||
<action android:name="com.twofortyfouram.locale.intent.action.FIRE_SETTING" />
|
||||
</intent-filter>
|
||||
|
||||
@@ -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
|
||||
com.devolver.reigns2
|
||||
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
|
||||
android
|
||||
au.com.shiftyjelly.pocketcasts
|
||||
com.google.android.gms
|
||||
com.android.providers.telephony
|
||||
com.resilio.sync
|
||||
com.google.android.apps.googlevoice
|
||||
com.discord
|
||||
com.cradle.iitc_mobile
|
||||
bbc.mobile.news.ww
|
||||
be.mygod.vpnhotspot
|
||||
ch.protonmail.android
|
||||
co.wanqu.android
|
||||
com.alphainventor.filemanager
|
||||
com.amazon.kindle
|
||||
com.amazon.mshop.android.shopping
|
||||
com.android.chrome
|
||||
com.android.providers.downloads
|
||||
com.android.providers.downloads.ui
|
||||
com.android.providers.telephony
|
||||
com.android.settings
|
||||
com.android.vending
|
||||
com.android6park.m6park
|
||||
com.apkpure.aegon
|
||||
com.apkupdater
|
||||
com.app.pornhub
|
||||
com.arthurivanets.owly
|
||||
com.asahi.tida.tablet
|
||||
com.authy.authy
|
||||
com.avmovie
|
||||
com.ballistiq.artstation
|
||||
com.binance.dev
|
||||
com.bitly.app
|
||||
com.brave.browser
|
||||
com.brave.browser_beta
|
||||
com.breel.wallpapers18
|
||||
com.bvanced.android.youtube
|
||||
com.chrome.beta
|
||||
com.chrome.canary
|
||||
com.chrome.dev
|
||||
com.cl.newt66y
|
||||
com.cradle.iitc_mobile
|
||||
com.cygames.shadowverse
|
||||
com.devhd.feedly
|
||||
com.devolver.reigns2
|
||||
com.discord
|
||||
com.downloader.video.tumblr
|
||||
com.driverbrowser
|
||||
com.dropbox.android
|
||||
com.duolingo
|
||||
com.duckduckgo.mobile.android
|
||||
com.dv.adm
|
||||
com.estrongs.android.pop
|
||||
com.estrongs.android.pop.pro
|
||||
com.evernote
|
||||
com.facebook.katana
|
||||
com.facebook.lite
|
||||
com.facebook.mlite
|
||||
com.facebook.orca
|
||||
com.facebook.services
|
||||
com.facebook.system
|
||||
com.fastaccess.github
|
||||
com.felixfilip.scpae
|
||||
com.fireproofstudios.theroom4
|
||||
com.firstrowria.pushnotificationtester
|
||||
com.flyersoft.moonreaderp
|
||||
com.fooview.android.fooview
|
||||
com.fvd.eversync
|
||||
com.gameloft.android.anmp.glofta8hm
|
||||
com.gameloft.android.anmp.glofta9hm
|
||||
com.gianlu.aria2app
|
||||
com.github.yeriomin.yalpstore
|
||||
com.google.android.apps.adm
|
||||
com.google.android.apps.books
|
||||
com.google.android.apps.docs
|
||||
com.google.android.apps.docs.editors.sheets
|
||||
com.google.android.apps.fitness
|
||||
com.google.android.apps.googleassistant
|
||||
com.google.android.apps.googlevoice
|
||||
com.google.android.apps.hangoutsdialer
|
||||
com.google.android.apps.inbox
|
||||
com.google.android.apps.magazines
|
||||
com.google.android.apps.maps
|
||||
com.google.android.apps.nbu.files
|
||||
com.google.android.apps.paidtasks
|
||||
com.google.android.apps.pdfviewer
|
||||
com.google.android.apps.photos
|
||||
com.google.android.apps.plus
|
||||
com.google.android.apps.translate
|
||||
com.google.android.gm
|
||||
com.google.android.gms
|
||||
com.google.android.gms.setup
|
||||
com.google.android.googlequicksearchbox
|
||||
com.google.android.gsf
|
||||
com.google.android.gsf.login
|
||||
com.google.android.ims
|
||||
com.google.android.inputmethod.latin
|
||||
com.google.android.instantapps.supervisor
|
||||
com.google.android.keep
|
||||
com.google.android.music
|
||||
com.google.android.ogyoutube
|
||||
com.google.android.partnersetup
|
||||
com.google.android.play.games
|
||||
com.google.android.street
|
||||
com.google.android.syncadapters.calendar
|
||||
com.google.android.syncadapters.contacts
|
||||
com.google.android.talk
|
||||
com.google.android.tts
|
||||
com.google.android.videos
|
||||
com.google.android.youtube
|
||||
com.google.ar.lens
|
||||
com.hochan.coldsoup
|
||||
com.ifttt.ifttt
|
||||
com.imgur.mobile
|
||||
com.innologica.inoreader
|
||||
com.instagram.android
|
||||
com.instapaper.android
|
||||
com.jarvanh.vpntether
|
||||
com.kapp.youtube.final
|
||||
com.klinker.android.twitter_l
|
||||
com.lastpass.lpandroid
|
||||
com.linecorp.linelite
|
||||
com.lingodeer
|
||||
com.mediapods.tumbpods
|
||||
com.mgoogle.android.gms
|
||||
com.microsoft.emmx
|
||||
com.microsoft.office.powerpoint
|
||||
com.microsoft.skydrive
|
||||
com.mixplorer
|
||||
com.msd.consumerchinese
|
||||
com.msd.professionalchinese
|
||||
com.mss2011c.sharehelper
|
||||
com.netflix.mediaclient
|
||||
com.newin.nplayer.pro
|
||||
com.nianticlabs.ingress.prime.qa
|
||||
com.nianticproject.ingress
|
||||
com.ninefolders.hd3
|
||||
com.ninegag.android.app
|
||||
com.nintendo.zara
|
||||
com.nytimes.cn
|
||||
com.oasisfeng.island
|
||||
com.ocnt.liveapp.hw
|
||||
com.orekie.search
|
||||
com.patreon.android
|
||||
com.paypal.android.p2pmobile
|
||||
com.perol.asdpl.pixivez
|
||||
com.pinterest
|
||||
com.popularapp.periodcalendar
|
||||
com.popularapp.videodownloaderforinstagram
|
||||
com.pushbullet.android
|
||||
com.quoord.tapatalkpro.activity
|
||||
com.quora.android
|
||||
com.rayark.cytus2
|
||||
com.rayark.implosion
|
||||
com.rayark.pluto
|
||||
com.reddit.frontpage
|
||||
com.resilio.sync
|
||||
com.rhmsoft.edit
|
||||
com.rubenmayayo.reddit
|
||||
com.sec.android.app.sbrowser
|
||||
com.sec.android.app.sbrowser.beta
|
||||
com.shanga.walli
|
||||
com.simplehabit.simplehabitapp
|
||||
com.slack
|
||||
com.snaptube.premium
|
||||
com.sololearn
|
||||
com.sonelli.juicessh
|
||||
com.spotify.music
|
||||
com.tencent.huatuo
|
||||
com.termux
|
||||
com.teslacoilsw.launcher
|
||||
com.theinitium.news
|
||||
com.thomsonreuters.reuters
|
||||
com.thunkable.android.hritvik00.freenom
|
||||
com.topjohnwu.magisk
|
||||
com.tripadvisor.tripadvisor
|
||||
com.tumblr
|
||||
com.twitter.android
|
||||
com.u91porn
|
||||
com.u9porn
|
||||
com.ubisoft.dance.justdance2015companion
|
||||
com.utopia.pxview
|
||||
com.valvesoftware.android.steam.communimunity
|
||||
com.valvesoftware.android.steam.community
|
||||
com.vanced.android.youtube
|
||||
com.vimeo.android.videoapp
|
||||
com.vivaldi.browser
|
||||
com.vivaldi.browser.snapshot
|
||||
com.vkontakte.android
|
||||
com.whatsapp
|
||||
com.wire
|
||||
com.wuxiangai.refactor
|
||||
com.xda.labs
|
||||
com.xvideos.app
|
||||
com.yandex.browser
|
||||
com.yandex.browser.beta
|
||||
com.yandex.browser.alpha
|
||||
com.z28j.feel
|
||||
con.medium.reader
|
||||
de.apkgrabber
|
||||
de.robv.android.xposed.installer
|
||||
dk.tacit.android.foldersync.full
|
||||
es.rafalense.telegram.themes
|
||||
es.rafalense.themes
|
||||
flipboard.app
|
||||
fm.moon.app
|
||||
fr.gouv.etalab.mastodon
|
||||
github.tornaco.xposedmoduletest
|
||||
idm.internet.download.manager
|
||||
idm.internet.download.manager.plus
|
||||
io.github.javiewer
|
||||
io.github.skyhacker2.magnetsearch
|
||||
io.va.exposed
|
||||
it.mvilla.android.fenix2
|
||||
jp.bokete.app.android
|
||||
jp.naver.line.android
|
||||
jp.pxv.android
|
||||
luo.speedometergpspro
|
||||
mark.via.gp
|
||||
me.tshine.easymark
|
||||
net.teeha.android.url_shortener
|
||||
net.tsapps.appsales
|
||||
onion.fire
|
||||
org.fdroid.fdroid
|
||||
org.freedownloadmanager.fdm
|
||||
org.kustom.widget
|
||||
org.mozilla.fennec_aurora
|
||||
org.mozilla.fenix
|
||||
org.mozilla.fenix.nightly
|
||||
org.mozilla.firefox
|
||||
org.mozilla.firefox_beta
|
||||
org.mozilla.focus
|
||||
org.schabi.newpipe
|
||||
org.telegram.messenger
|
||||
org.telegram.multi
|
||||
org.telegram.plus
|
||||
org.thunderdog.challegram
|
||||
org.torproject.android
|
||||
org.torproject.torbrowser_alpha
|
||||
org.wikipedia
|
||||
org.xbmc.kodi
|
||||
pl.zdunex25.updater
|
||||
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
|
||||
com.shanga.walli
|
||||
com.whatsapp
|
||||
com.wire
|
||||
com.simplehabit.simplehabitapp
|
||||
tw.com.gamer.android.activecenter
|
||||
videodownloader.downloadvideo.downloader
|
||||
uk.co.bbc.learningenglish
|
||||
com.ted.android
|
||||
|
||||
@@ -13,8 +13,8 @@
|
||||
}
|
||||
},
|
||||
"system": {
|
||||
"statsInboundUplink": true,
|
||||
"statsInboundDownlink": true
|
||||
"statsOutboundUplink": true,
|
||||
"statsOutboundDownlink": true
|
||||
}
|
||||
},
|
||||
"inbounds": [{
|
||||
@@ -54,7 +54,7 @@
|
||||
"users": [
|
||||
{
|
||||
"id": "a3482e88-686a-4a58-8126-99c9df64b7bf",
|
||||
"alterId": 64,
|
||||
"alterId": 0,
|
||||
"security": "auto",
|
||||
"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;
|
||||
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.support.v7.widget.helper.ItemTouchHelper;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.recyclerview.widget.ItemTouchHelper;
|
||||
|
||||
/**
|
||||
* 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);
|
||||
|
||||
|
||||
void onItemMoveCompleted();
|
||||
|
||||
/**
|
||||
* Called when an item has been dismissed by a swipe.<br/>
|
||||
* <br/>
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
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
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
package com.v2ray.ang.helper;
|
||||
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
/**
|
||||
* Listener for manual initiation of a drag.
|
||||
|
||||
@@ -17,9 +17,11 @@
|
||||
package com.v2ray.ang.helper;
|
||||
|
||||
import android.graphics.Canvas;
|
||||
import android.support.v7.widget.GridLayoutManager;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.support.v7.widget.helper.ItemTouchHelper;
|
||||
import androidx.recyclerview.widget.GridLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.recyclerview.widget.ItemTouchHelper;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
/**
|
||||
* An implementation of {@link ItemTouchHelper.Callback} that enables basic drag & drop and
|
||||
@@ -52,7 +54,7 @@ public class SimpleItemTouchHelperCallback extends ItemTouchHelper.Callback {
|
||||
}
|
||||
|
||||
@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
|
||||
if (recyclerView.getLayoutManager() instanceof GridLayoutManager) {
|
||||
final int dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN | ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT;
|
||||
@@ -66,24 +68,25 @@ public class SimpleItemTouchHelperCallback extends ItemTouchHelper.Callback {
|
||||
}
|
||||
|
||||
@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()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Notify the adapter of the move
|
||||
mAdapter.onItemMove(source.getAdapterPosition(), target.getAdapterPosition());
|
||||
mAdapter.onItemMove(source.getBindingAdapterPosition(), target.getBindingAdapterPosition());
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSwiped(RecyclerView.ViewHolder viewHolder, int i) {
|
||||
// Notify the adapter of the dismissal
|
||||
mAdapter.onItemDismiss(viewHolder.getAdapterPosition());
|
||||
mAdapter.onItemDismiss(viewHolder.getBindingAdapterPosition());
|
||||
}
|
||||
|
||||
@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) {
|
||||
// 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();
|
||||
@@ -109,9 +112,11 @@ public class SimpleItemTouchHelperCallback extends ItemTouchHelper.Callback {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearView(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
|
||||
public void clearView(@NotNull RecyclerView recyclerView, @NotNull RecyclerView.ViewHolder viewHolder) {
|
||||
super.clearView(recyclerView, viewHolder);
|
||||
|
||||
mAdapter.onItemMoveCompleted();
|
||||
|
||||
viewHolder.itemView.setAlpha(ALPHA_FULL);
|
||||
|
||||
if (viewHolder instanceof ItemTouchHelperViewHolder) {
|
||||
|
||||
@@ -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
|
||||
|
||||
import android.app.Application
|
||||
//import com.squareup.leakcanary.LeakCanary
|
||||
import com.v2ray.ang.util.AngConfigManager
|
||||
import me.dozen.dpreference.DPreference
|
||||
import org.jetbrains.anko.defaultSharedPreferences
|
||||
import androidx.multidex.MultiDexApplication
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.tencent.mmkv.MMKV
|
||||
|
||||
class AngApplication : Application() {
|
||||
class AngApplication : MultiDexApplication() {
|
||||
companion object {
|
||||
const val PREF_LAST_VERSION = "pref_last_version"
|
||||
}
|
||||
|
||||
var curIndex = -1 //Current proxy that is opened. (Used to implement restart feature)
|
||||
var firstRun = false
|
||||
private set
|
||||
|
||||
val defaultDPreference by lazy { DPreference(this, packageName + "_preferences") }
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
|
||||
// LeakCanary.install(this)
|
||||
|
||||
val defaultSharedPreferences = PreferenceManager.getDefaultSharedPreferences(this)
|
||||
firstRun = defaultSharedPreferences.getInt(PREF_LAST_VERSION, 0) != BuildConfig.VERSION_CODE
|
||||
if (firstRun)
|
||||
defaultSharedPreferences.edit().putInt(PREF_LAST_VERSION, BuildConfig.VERSION_CODE).apply()
|
||||
|
||||
//Logger.init().logLevel(if (BuildConfig.DEBUG) LogLevel.FULL else LogLevel.NONE)
|
||||
AngConfigManager.inject(this)
|
||||
MMKV.initialize(this)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,15 +6,43 @@ package com.v2ray.ang
|
||||
*/
|
||||
object AppConfig {
|
||||
const val ANG_PACKAGE = "com.v2ray.ang"
|
||||
const val DIR_ASSETS = "assets"
|
||||
|
||||
// legacy
|
||||
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 VMESS_PROTOCOL: String = "vmess://"
|
||||
const val SS_PROTOCOL: String = "ss://"
|
||||
const val SOCKS_PROTOCOL: String = "socks://"
|
||||
const val PREF_ROUTING_CUSTOM = "pref_routing_custom"
|
||||
|
||||
// Preferences mapped to MMKV
|
||||
const val PREF_MODE = "pref_mode"
|
||||
const val PREF_SPEED_ENABLED = "pref_speed_enabled"
|
||||
const val PREF_SNIFFING_ENABLED = "pref_sniffing_enabled"
|
||||
const val PREF_PROXY_SHARING = "pref_proxy_sharing_enabled"
|
||||
const val PREF_LOCAL_DNS_ENABLED = "pref_local_dns_enabled"
|
||||
const val PREF_FAKE_DNS_ENABLED = "pref_fake_dns_enabled"
|
||||
const val PREF_VPN_DNS = "pref_vpn_dns"
|
||||
const val PREF_REMOTE_DNS = "pref_remote_dns"
|
||||
const val PREF_DOMESTIC_DNS = "pref_domestic_dns"
|
||||
const val PREF_LOCAL_DNS_PORT = "pref_local_dns_port"
|
||||
const val PREF_ALLOW_INSECURE = "pref_allow_insecure"
|
||||
const val PREF_SOCKS_PORT = "pref_socks_port"
|
||||
const val PREF_HTTP_PORT = "pref_http_port"
|
||||
const val PREF_LOGLEVEL = "pref_core_loglevel"
|
||||
const val PREF_LANGUAGE = "pref_language"
|
||||
const val PREF_PREFER_IPV6 = "pref_prefer_ipv6"
|
||||
const val PREF_ROUTING_DOMAIN_STRATEGY = "pref_routing_domain_strategy"
|
||||
const val PREF_ROUTING_MODE = "pref_routing_mode"
|
||||
const val PREF_V2RAY_ROUTING_AGENT = "pref_v2ray_routing_agent"
|
||||
const val PREF_V2RAY_ROUTING_DIRECT = "pref_v2ray_routing_direct"
|
||||
const val PREF_V2RAY_ROUTING_BLOCKED = "pref_v2ray_routing_blocked"
|
||||
const val PREF_PER_APP_PROXY = "pref_per_app_proxy"
|
||||
const val PREF_PER_APP_PROXY_SET = "pref_per_app_proxy_set"
|
||||
const val PREF_BYPASS_APPS = "pref_bypass_apps"
|
||||
const val PREF_CONFIRM_REMOVE = "pref_confirm_remove"
|
||||
|
||||
const val HTTP_PROTOCOL: String = "http://"
|
||||
const val HTTPS_PROTOCOL: String = "https://"
|
||||
|
||||
const val BROADCAST_ACTION_SERVICE = "com.v2ray.ang.action.service"
|
||||
const val BROADCAST_ACTION_ACTIVITY = "com.v2ray.ang.action.activity"
|
||||
const val BROADCAST_ACTION_WIDGET_CLICK = "com.v2ray.ang.action.widget.click"
|
||||
@@ -25,9 +53,6 @@ object AppConfig {
|
||||
const val TASKER_EXTRA_BUNDLE_GUID = "tasker_extra_bundle_guid"
|
||||
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_DIRECT = "direct"
|
||||
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 v2rayCustomRoutingListUrl = "https://raw.githubusercontent.com/2dust/v2rayCustomRoutingList/master/"
|
||||
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_DIRECT = "223.5.5.5"
|
||||
|
||||
const val PORT_LOCAL_DNS = "10853"
|
||||
const val PORT_SOCKS = "10808"
|
||||
const val PORT_HTTP = "10809"
|
||||
|
||||
const val MSG_REGISTER_CLIENT = 1
|
||||
const val MSG_STATE_RUNNING = 11
|
||||
const val MSG_STATE_NOT_RUNNING = 12
|
||||
@@ -50,12 +81,9 @@ object AppConfig {
|
||||
const val MSG_STATE_STOP = 4
|
||||
const val MSG_STATE_STOP_SUCCESS = 41
|
||||
const val MSG_STATE_RESTART = 5
|
||||
|
||||
object EConfigType {
|
||||
val Vmess = 1
|
||||
val Custom = 2
|
||||
val Shadowsocks = 3
|
||||
val Socks = 4
|
||||
}
|
||||
|
||||
const val MSG_MEASURE_DELAY = 6
|
||||
const val MSG_MEASURE_DELAY_SUCCESS = 61
|
||||
const val MSG_MEASURE_CONFIG = 7
|
||||
const val MSG_MEASURE_CONFIG_SUCCESS = 71
|
||||
const val MSG_MEASURE_CONFIG_CANCEL = 72
|
||||
}
|
||||
@@ -17,12 +17,16 @@ data class AngConfig(
|
||||
var requestHost: String = "",
|
||||
var path: String = "",
|
||||
var streamSecurity: String = "",
|
||||
var allowInsecure: String = "",
|
||||
var configType: Int = 1,
|
||||
var configVersion: Int = 1,
|
||||
var testResult: String = "",
|
||||
var subid: String = "")
|
||||
var subid: String = "",
|
||||
var flow: String = "",
|
||||
var sni: String = "")
|
||||
|
||||
data class SubItemBean(var id: 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
|
||||
|
||||
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(
|
||||
val stats: Any?=null,
|
||||
var stats: Any? = null,
|
||||
val log: LogBean,
|
||||
val policy: PolicyBean,
|
||||
var policy: PolicyBean?,
|
||||
val inbounds: ArrayList<InboundBean>,
|
||||
var outbounds: ArrayList<OutboundBean>,
|
||||
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,
|
||||
val error: String,
|
||||
val loglevel: String)
|
||||
var loglevel: String?,
|
||||
val dnsLog: Boolean? = null)
|
||||
|
||||
data class InboundBean(
|
||||
var tag: String,
|
||||
var port: Int,
|
||||
var protocol: String,
|
||||
var listen: String?=null,
|
||||
val settings: InSettingsBean,
|
||||
val sniffing: SniffingBean?) {
|
||||
var listen: String? = null,
|
||||
val settings: Any? = null,
|
||||
val sniffing: SniffingBean?,
|
||||
val streamSettings: Any? = null,
|
||||
val allocate: Any? = null) {
|
||||
|
||||
data class InSettingsBean(val auth: String? = null,
|
||||
val udp: Boolean? = null,
|
||||
val userLevel: Int? =null,
|
||||
val userLevel: Int? = null,
|
||||
val address: String? = null,
|
||||
val port: Int? = null,
|
||||
val network: String? = null)
|
||||
|
||||
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 settings: OutSettingsBean?,
|
||||
var streamSettings: StreamSettingsBean?,
|
||||
var mux: MuxBean?) {
|
||||
var settings: OutSettingsBean? = null,
|
||||
var streamSettings: StreamSettingsBean? = null,
|
||||
val proxySettings: Any? = null,
|
||||
val sendThrough: String? = null,
|
||||
val mux: MuxBean? = MuxBean(false)) {
|
||||
|
||||
data class OutSettingsBean(var vnext: List<VnextBean>?,
|
||||
var servers: List<ServersBean>?,
|
||||
var response: Response) {
|
||||
data class OutSettingsBean(var vnext: List<VnextBean>? = null,
|
||||
var servers: List<ServersBean>? = null,
|
||||
/*Blackhole*/
|
||||
var response: Response? = null,
|
||||
/*DNS*/
|
||||
val network: String? = null,
|
||||
val address: 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,
|
||||
var port: Int,
|
||||
data class VnextBean(var address: String = "",
|
||||
var port: Int = DEFAULT_PORT,
|
||||
var users: List<UsersBean>) {
|
||||
|
||||
data class UsersBean(var id: String,
|
||||
var alterId: Int,
|
||||
var security: String,
|
||||
var level: Int)
|
||||
data class UsersBean(var id: String = "",
|
||||
var alterId: Int? = null,
|
||||
var security: String = DEFAULT_SECURITY,
|
||||
var level: Int = DEFAULT_LEVEL,
|
||||
var encryption: String = "",
|
||||
var flow: String = "")
|
||||
}
|
||||
|
||||
data class ServersBean(var address: String,
|
||||
var method: String,
|
||||
var ota: Boolean,
|
||||
var password: String,
|
||||
var port: Int,
|
||||
var level: Int)
|
||||
data class ServersBean(var address: String = "",
|
||||
var method: String = "chacha20-poly1305",
|
||||
var ota: Boolean = false,
|
||||
var password: String = "",
|
||||
var port: Int = DEFAULT_PORT,
|
||||
var level: Int = DEFAULT_LEVEL,
|
||||
val email: String? = null,
|
||||
var flow: String? = null,
|
||||
val ivCheck: Boolean? = null,
|
||||
var users: List<SocksUsersBean>? = null) {
|
||||
|
||||
|
||||
data class SocksUsersBean(var user: String = "",
|
||||
var pass: String = "",
|
||||
var level: Int = DEFAULT_LEVEL)
|
||||
}
|
||||
|
||||
data class Response(var type: String)
|
||||
}
|
||||
|
||||
data class StreamSettingsBean(var network: String,
|
||||
var security: String,
|
||||
var tcpSettings: TcpsettingsBean?,
|
||||
var kcpsettings: KcpsettingsBean?,
|
||||
var wssettings: WssettingsBean?,
|
||||
var httpsettings: HttpsettingsBean?,
|
||||
var tlssettings: TlssettingsBean?,
|
||||
var quicsettings: QuicsettingBean?
|
||||
data class StreamSettingsBean(var network: String = DEFAULT_NETWORK,
|
||||
var security: String = "",
|
||||
var tcpSettings: TcpSettingsBean? = null,
|
||||
var kcpSettings: KcpSettingsBean? = null,
|
||||
var wsSettings: WsSettingsBean? = null,
|
||||
var httpSettings: HttpSettingsBean? = null,
|
||||
var tlsSettings: TlsSettingsBean? = null,
|
||||
var quicSettings: QuicSettingBean? = null,
|
||||
var xtlsSettings: TlsSettingsBean? = null,
|
||||
var grpcSettings: GrpcSettingsBean? = null,
|
||||
val dsSettings: Any? = null,
|
||||
val sockopt: Any? = null
|
||||
) {
|
||||
|
||||
data class TcpsettingsBean(var connectionReuse: Boolean = true,
|
||||
var header: HeaderBean = HeaderBean()) {
|
||||
data class TcpSettingsBean(var header: HeaderBean = HeaderBean(),
|
||||
val acceptProxyProtocol: Boolean? = null) {
|
||||
data class HeaderBean(var type: String = "none",
|
||||
var request: Any? = null,
|
||||
var response: Any? = null)
|
||||
var request: RequestBean? = null,
|
||||
var response: Any? = null) {
|
||||
data class RequestBean(var path: List<String> = ArrayList(),
|
||||
var headers: HeadersBean = HeadersBean(),
|
||||
val version: String? = null,
|
||||
val method: String? = null) {
|
||||
data class HeadersBean(var Host: List<String> = ArrayList(),
|
||||
@SerializedName("User-Agent")
|
||||
val userAgent: List<String>? = null,
|
||||
@SerializedName("Accept-Encoding")
|
||||
val acceptEncoding: List<String>? = null,
|
||||
val Connection: List<String>? = null,
|
||||
val Pragma: String? = null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class KcpsettingsBean(var mtu: Int = 1350,
|
||||
var tti: Int = 20,
|
||||
data class KcpSettingsBean(var mtu: Int = 1350,
|
||||
var tti: Int = 50,
|
||||
var uplinkCapacity: Int = 12,
|
||||
var downlinkCapacity: Int = 100,
|
||||
var congestion: Boolean = false,
|
||||
var readBufferSize: Int = 1,
|
||||
var writeBufferSize: Int = 1,
|
||||
var header: HeaderBean = HeaderBean()) {
|
||||
var header: HeaderBean = HeaderBean(),
|
||||
var seed: String? = null) {
|
||||
data class HeaderBean(var type: String = "none")
|
||||
}
|
||||
|
||||
data class WssettingsBean(var connectionReuse: Boolean = true,
|
||||
var path: String = "",
|
||||
var headers: HeadersBean = HeadersBean()) {
|
||||
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<String>(), var path: String = "")
|
||||
data class HttpSettingsBean(var host: List<String> = ArrayList(),
|
||||
var path: String = "")
|
||||
|
||||
data class TlssettingsBean(var allowInsecure: Boolean = true,
|
||||
var serverName: 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",
|
||||
data class QuicSettingBean(var security: String = "none",
|
||||
var key: String = "",
|
||||
var header: HeaderBean = HeaderBean()) {
|
||||
data class HeaderBean(var type: String = "none")
|
||||
}
|
||||
|
||||
data class GrpcSettingsBean(var serviceName: String = "",
|
||||
var multiMode: Boolean? = null)
|
||||
|
||||
fun populateTransportSettings(transport: String, headerType: String?, host: String?, path: String?, seed: String?,
|
||||
quicSecurity: String?, key: String?, mode: String?, serviceName: String?): String {
|
||||
var sni = ""
|
||||
network = transport
|
||||
when (network) {
|
||||
"tcp" -> {
|
||||
val tcpSetting = TcpSettingsBean()
|
||||
if (headerType == HTTP) {
|
||||
tcpSetting.header.type = HTTP
|
||||
if (!TextUtils.isEmpty(host) || !TextUtils.isEmpty(path)) {
|
||||
val requestObj = TcpSettingsBean.HeaderBean.RequestBean()
|
||||
requestObj.headers.Host = (host ?: "").split(",").map { it.trim() }.filter { it.isNotEmpty() }
|
||||
requestObj.path = (path ?: "").split(",").map { it.trim() }.filter { it.isNotEmpty() }
|
||||
tcpSetting.header.request = requestObj
|
||||
sni = requestObj.headers.Host.getOrNull(0) ?: sni
|
||||
}
|
||||
} else {
|
||||
tcpSetting.header.type = "none"
|
||||
sni = host ?: ""
|
||||
}
|
||||
tcpSettings = tcpSetting
|
||||
}
|
||||
"kcp" -> {
|
||||
val kcpsetting = KcpSettingsBean()
|
||||
kcpsetting.header.type = headerType ?: "none"
|
||||
if (seed.isNullOrEmpty()) {
|
||||
kcpsetting.seed = null
|
||||
} else {
|
||||
kcpsetting.seed = seed
|
||||
}
|
||||
kcpSettings = kcpsetting
|
||||
}
|
||||
"ws" -> {
|
||||
val wssetting = WsSettingsBean()
|
||||
wssetting.headers.Host = host ?: ""
|
||||
sni = wssetting.headers.Host
|
||||
wssetting.path = path ?: "/"
|
||||
wsSettings = wssetting
|
||||
}
|
||||
"h2", "http" -> {
|
||||
network = "h2"
|
||||
val h2Setting = HttpSettingsBean()
|
||||
h2Setting.host = (host ?: "").split(",").map { it.trim() }.filter { it.isNotEmpty() }
|
||||
sni = h2Setting.host.getOrNull(0) ?: sni
|
||||
h2Setting.path = path ?: "/"
|
||||
httpSettings = h2Setting
|
||||
}
|
||||
"quic" -> {
|
||||
val quicsetting = QuicSettingBean()
|
||||
quicsetting.security = quicSecurity ?: "none"
|
||||
quicsetting.key = key ?: ""
|
||||
quicsetting.header.type = headerType ?: "none"
|
||||
quicSettings = quicsetting
|
||||
}
|
||||
"grpc" -> {
|
||||
val grpcSetting = GrpcSettingsBean()
|
||||
grpcSetting.multiMode = mode == "multi"
|
||||
grpcSetting.serviceName = serviceName ?: ""
|
||||
sni = host ?: ""
|
||||
grpcSettings = grpcSetting
|
||||
}
|
||||
}
|
||||
return sni
|
||||
}
|
||||
|
||||
data class MuxBean(var enabled: Boolean)
|
||||
fun populateTlsSettings(streamSecurity: String, allowInsecure: Boolean, sni: String, fingerprint: String?, alpns: String?) {
|
||||
security = streamSecurity
|
||||
val tlsSetting = TlsSettingsBean(
|
||||
allowInsecure = allowInsecure,
|
||||
serverName = sni,
|
||||
fingerprint = fingerprint,
|
||||
alpn = if (alpns.isNullOrEmpty()) null else alpns.split(",").map { it.trim() }.filter { it.isNotEmpty() }
|
||||
)
|
||||
if (security == TLS) {
|
||||
tlsSettings = tlsSetting
|
||||
xtlsSettings = null
|
||||
} else if (security == XTLS) {
|
||||
tlsSettings = null
|
||||
xtlsSettings = tlsSetting
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//data class DnsBean(var servers: List<String>)
|
||||
data class DnsBean(var servers: List<Any>?=null,
|
||||
var hosts: Map<String, String>?=null
|
||||
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: ArrayList<Any>? = null,
|
||||
var hosts: Map<String, Any>? = null,
|
||||
val clientIp: String? = null,
|
||||
val disableCache: Boolean? = null,
|
||||
val queryStrategy: String? = null,
|
||||
val tag: String? = null
|
||||
) {
|
||||
data class ServersBean(var address: String = "",
|
||||
var port: Int = 0,
|
||||
var domains: List<String>?)
|
||||
var port: Int? = null,
|
||||
var domains: List<String>? = null,
|
||||
var expectIPs: List<String>? = null,
|
||||
val clientIp: String? = null)
|
||||
}
|
||||
|
||||
data class RoutingBean(var domainStrategy: String,
|
||||
var rules: ArrayList<RulesBean>) {
|
||||
var domainMatcher: String? = null,
|
||||
var rules: ArrayList<RulesBean>,
|
||||
val balancers: List<Any>? = null) {
|
||||
|
||||
data class RulesBean(var type: String = "",
|
||||
var ip: ArrayList<String>? = null,
|
||||
var domain: ArrayList<String>? = null,
|
||||
var outboundTag: String = "",
|
||||
var balancerTag: String? = null,
|
||||
var port: String? = null,
|
||||
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>,
|
||||
var system: Any?=null) {
|
||||
var system: Any? = null) {
|
||||
data class LevelBean(
|
||||
var handshake: Int? = null,
|
||||
var connIdle: 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 port: String = "",
|
||||
var id: String = "",
|
||||
var aid: String = "",
|
||||
var aid: String = "0",
|
||||
var scy: String = "",
|
||||
var net: String = "",
|
||||
var type: String = "",
|
||||
var host: 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.os.Build
|
||||
import android.widget.Toast
|
||||
import com.v2ray.ang.AngApplication
|
||||
import me.dozen.dpreference.DPreference
|
||||
import me.drakeet.support.toast.ToastCompat
|
||||
import org.json.JSONObject
|
||||
import java.net.URI
|
||||
import java.net.URLConnection
|
||||
|
||||
/**
|
||||
@@ -14,11 +16,19 @@ import java.net.URLConnection
|
||||
val Context.v2RayApplication: AngApplication
|
||||
get() = applicationContext as AngApplication
|
||||
|
||||
val Context.defaultDPreference: DPreference
|
||||
get() = v2RayApplication.defaultDPreference
|
||||
fun Context.toast(message: Int): Toast = ToastCompat
|
||||
.makeText(this, message, Toast.LENGTH_SHORT)
|
||||
.apply {
|
||||
show()
|
||||
}
|
||||
|
||||
fun Context.toast(message: CharSequence): Toast = ToastCompat
|
||||
.makeText(this, message, Toast.LENGTH_SHORT)
|
||||
.apply {
|
||||
show()
|
||||
}
|
||||
|
||||
fun JSONObject.putOpt(pair: Pair<String, Any>) = putOpt(pair.first, pair.second)!!
|
||||
fun JSONObject.putOpt(pair: Pair<String, Any>) = putOpt(pair.first, pair.second)
|
||||
fun JSONObject.putOpt(pairs: Map<String, Any>) = pairs.forEach { putOpt(it.key to it.value) }
|
||||
|
||||
const val threshold = 1000
|
||||
@@ -27,38 +37,44 @@ const val divisor = 1024F
|
||||
fun Long.toSpeedString() = toTrafficString() + "/s"
|
||||
|
||||
fun Long.toTrafficString(): String {
|
||||
if (this == 0L)
|
||||
return "\t\t\t0\t B"
|
||||
|
||||
if (this < threshold)
|
||||
return "$this B"
|
||||
return "${this.toFloat().toShortString()}\t B"
|
||||
|
||||
val kib = this / divisor
|
||||
if (kib < threshold)
|
||||
return "${kib.toShortString()} KB"
|
||||
return "${kib.toShortString()}\t KB"
|
||||
|
||||
val mib = kib / divisor
|
||||
if (mib < threshold)
|
||||
return "${mib.toShortString()} MB"
|
||||
return "${mib.toShortString()}\t MB"
|
||||
|
||||
val gib = mib / divisor
|
||||
if (gib < threshold)
|
||||
return "${gib.toShortString()} GB"
|
||||
return "${gib.toShortString()}\t GB"
|
||||
|
||||
val tib = gib / divisor
|
||||
if (tib < threshold)
|
||||
return "${tib.toShortString()} TB"
|
||||
return "${tib.toShortString()}\t TB"
|
||||
|
||||
val pib = tib / divisor
|
||||
if (pib < threshold)
|
||||
return "${pib.toShortString()} PB"
|
||||
return "${pib.toShortString()}\t PB"
|
||||
|
||||
return "∞"
|
||||
}
|
||||
|
||||
private fun Float.toShortString(): String {
|
||||
val s = toString()
|
||||
val s = "%.2f".format(this)
|
||||
if (s.length <= 4)
|
||||
return s
|
||||
return s.substring(0, 4).removeSuffix(".")
|
||||
}
|
||||
|
||||
val URLConnection.responseLength: Long
|
||||
get() = if (Build.VERSION.SDK_INT >= 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.text.TextUtils
|
||||
import com.google.zxing.WriterException
|
||||
import com.tencent.mmkv.MMKV
|
||||
import com.v2ray.ang.AppConfig
|
||||
import com.v2ray.ang.service.V2RayServiceManager
|
||||
import com.v2ray.ang.util.MmkvManager
|
||||
|
||||
import com.v2ray.ang.util.Utils
|
||||
|
||||
class TaskerReceiver : BroadcastReceiver() {
|
||||
private val mainStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_MAIN, MMKV.MULTI_PROCESS_MODE) }
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent?) {
|
||||
|
||||
try {
|
||||
@@ -21,9 +26,10 @@ class TaskerReceiver : BroadcastReceiver() {
|
||||
return
|
||||
} else if (switch) {
|
||||
if (guid == AppConfig.TASKER_DEFAULT_GUID) {
|
||||
Utils.startVService(context)
|
||||
Utils.startVServiceFromToggle(context)
|
||||
} else {
|
||||
Utils.startVService(context, guid)
|
||||
mainStorage?.encode(MmkvManager.KEY_SELECTED_SERVER, guid)
|
||||
V2RayServiceManager.startV2Ray(context)
|
||||
}
|
||||
} else {
|
||||
Utils.stopVService(context)
|
||||
|
||||
@@ -3,14 +3,15 @@ package com.v2ray.ang.receiver
|
||||
import android.app.PendingIntent
|
||||
import android.appwidget.AppWidgetManager
|
||||
import android.appwidget.AppWidgetProvider
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.os.Build
|
||||
import android.widget.RemoteViews
|
||||
import com.v2ray.ang.R
|
||||
import com.v2ray.ang.AppConfig
|
||||
import com.v2ray.ang.service.V2RayServiceManager
|
||||
import com.v2ray.ang.util.Utils
|
||||
import org.jetbrains.anko.toast
|
||||
|
||||
class WidgetProvider : AppWidgetProvider() {
|
||||
/**
|
||||
@@ -18,11 +19,37 @@ class WidgetProvider : AppWidgetProvider() {
|
||||
*/
|
||||
override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) {
|
||||
super.onUpdate(context, appWidgetManager, appWidgetIds)
|
||||
updateWidgetBackground(context, appWidgetManager, appWidgetIds, V2RayServiceManager.v2rayPoint.isRunning)
|
||||
}
|
||||
|
||||
|
||||
private fun updateWidgetBackground(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray, isRunning: Boolean) {
|
||||
val remoteViews = RemoteViews(context.packageName, R.layout.widget_switch)
|
||||
val intent = Intent(AppConfig.BROADCAST_ACTION_WIDGET_CLICK)
|
||||
val pendingIntent = PendingIntent.getBroadcast(context, R.id.layout_switch, intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||
val intent = Intent(context, WidgetProvider::class.java)
|
||||
intent.action = AppConfig.BROADCAST_ACTION_WIDGET_CLICK
|
||||
val pendingIntent = PendingIntent.getBroadcast(
|
||||
context,
|
||||
R.id.layout_switch,
|
||||
intent,
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
||||
} else {
|
||||
PendingIntent.FLAG_UPDATE_CURRENT
|
||||
})
|
||||
remoteViews.setOnClickPendingIntent(R.id.layout_switch, pendingIntent)
|
||||
if (isRunning) {
|
||||
remoteViews.setInt(
|
||||
R.id.layout_switch,
|
||||
"setBackgroundResource",
|
||||
R.drawable.ic_rounded_corner_theme
|
||||
)
|
||||
} else {
|
||||
remoteViews.setInt(
|
||||
R.id.layout_switch,
|
||||
"setBackgroundResource",
|
||||
R.drawable.ic_rounded_corner_grey
|
||||
)
|
||||
}
|
||||
|
||||
for (appWidgetId in appWidgetIds) {
|
||||
appWidgetManager.updateAppWidget(appWidgetId, remoteViews)
|
||||
@@ -30,21 +57,29 @@ class WidgetProvider : AppWidgetProvider() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 接收窗口小部件点击时发送的广播
|
||||
* 接收窗口小部件发送的广播
|
||||
*/
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
super.onReceive(context, intent)
|
||||
if (AppConfig.BROADCAST_ACTION_WIDGET_CLICK == intent.action) {
|
||||
|
||||
val isRunning = Utils.isServiceRun(context, "com.v2ray.ang.service.V2RayVpnService")
|
||||
if (isRunning) {
|
||||
// context.toast(R.string.toast_services_stop)
|
||||
if (V2RayServiceManager.v2rayPoint.isRunning) {
|
||||
Utils.stopVService(context)
|
||||
} else {
|
||||
// context.toast(R.string.toast_services_start)
|
||||
Utils.startVService(context)
|
||||
Utils.startVServiceFromToggle(context)
|
||||
}
|
||||
} else if (AppConfig.BROADCAST_ACTION_ACTIVITY == intent.action) {
|
||||
AppWidgetManager.getInstance(context)?.let { manager ->
|
||||
when (intent.getIntExtra("key", 0)) {
|
||||
AppConfig.MSG_STATE_RUNNING, AppConfig.MSG_STATE_START_SUCCESS -> {
|
||||
updateWidgetBackground(context, manager, manager.getAppWidgetIds(ComponentName(context, WidgetProvider::class.java)),
|
||||
true)
|
||||
}
|
||||
AppConfig.MSG_STATE_NOT_RUNNING, AppConfig.MSG_STATE_START_FAILURE, AppConfig.MSG_STATE_STOP_SUCCESS -> {
|
||||
updateWidgetBackground(context, manager, manager.getAppWidgetIds(ComponentName(context, WidgetProvider::class.java)),
|
||||
false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -6,19 +6,15 @@ import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.graphics.drawable.Icon
|
||||
import android.net.VpnService
|
||||
import android.os.Build
|
||||
import android.service.quicksettings.Tile
|
||||
import android.service.quicksettings.TileService
|
||||
import com.v2ray.ang.AppConfig
|
||||
import com.v2ray.ang.R
|
||||
import com.v2ray.ang.extension.defaultDPreference
|
||||
import com.v2ray.ang.util.MessageUtil
|
||||
import com.v2ray.ang.util.Utils
|
||||
import org.jetbrains.anko.toast
|
||||
import java.lang.ref.SoftReference
|
||||
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.N)
|
||||
class QSTileService : TileService() {
|
||||
|
||||
@@ -26,14 +22,13 @@ class QSTileService : TileService() {
|
||||
if (state == Tile.STATE_INACTIVE) {
|
||||
qsTile?.state = Tile.STATE_INACTIVE
|
||||
qsTile?.label = getString(R.string.app_name)
|
||||
qsTile?.icon = Icon.createWithResource(applicationContext, R.drawable.ic_v_idle)
|
||||
qsTile?.icon = Icon.createWithResource(applicationContext, R.drawable.ic_stat_name)
|
||||
} else if (state == Tile.STATE_ACTIVE) {
|
||||
qsTile?.state = Tile.STATE_ACTIVE
|
||||
qsTile?.label = defaultDPreference.getPrefString(AppConfig.PREF_CURR_CONFIG_NAME, "NG")
|
||||
qsTile?.icon = Icon.createWithResource(applicationContext, R.drawable.ic_v)
|
||||
qsTile?.label = V2RayServiceManager.currentConfig?.remarks
|
||||
qsTile?.icon = Icon.createWithResource(applicationContext, R.drawable.ic_stat_name)
|
||||
}
|
||||
|
||||
|
||||
qsTile?.updateTile()
|
||||
}
|
||||
|
||||
@@ -56,11 +51,7 @@ class QSTileService : TileService() {
|
||||
super.onClick()
|
||||
when (qsTile.state) {
|
||||
Tile.STATE_INACTIVE -> {
|
||||
val intent = VpnService.prepare(this)
|
||||
if (intent == null)
|
||||
if (!Utils.startVService(this)) {
|
||||
toast(R.string.app_tile_first_use)
|
||||
}
|
||||
Utils.startVServiceFromToggle(this)
|
||||
}
|
||||
Tile.STATE_ACTIVE -> {
|
||||
Utils.stopVService(this)
|
||||
|
||||
@@ -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,71 +1,46 @@
|
||||
package com.v2ray.ang.service
|
||||
|
||||
import android.app.Notification
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.content.BroadcastReceiver
|
||||
import android.app.Service
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.content.pm.PackageManager
|
||||
import android.graphics.Color
|
||||
import android.net.*
|
||||
import android.net.VpnService
|
||||
import android.os.*
|
||||
import android.support.annotation.RequiresApi
|
||||
import android.support.v4.app.NotificationCompat
|
||||
import android.os.Build
|
||||
import android.os.ParcelFileDescriptor
|
||||
import android.os.StrictMode
|
||||
import android.util.Log
|
||||
import androidx.annotation.RequiresApi
|
||||
import com.tencent.mmkv.MMKV
|
||||
import com.v2ray.ang.AppConfig
|
||||
import com.v2ray.ang.R
|
||||
import com.v2ray.ang.extension.defaultDPreference
|
||||
import com.v2ray.ang.extension.toSpeedString
|
||||
import com.v2ray.ang.ui.MainActivity
|
||||
import com.v2ray.ang.ui.PerAppProxyActivity
|
||||
import com.v2ray.ang.ui.SettingsActivity
|
||||
import com.v2ray.ang.util.MessageUtil
|
||||
import com.v2ray.ang.dto.ERoutingMode
|
||||
import com.v2ray.ang.util.MmkvManager
|
||||
import com.v2ray.ang.util.MyContextWrapper
|
||||
import com.v2ray.ang.util.Utils
|
||||
import libv2ray.Libv2ray
|
||||
import libv2ray.V2RayVPNServiceSupportsSet
|
||||
import rx.Observable
|
||||
import rx.Subscription
|
||||
import java.net.InetAddress
|
||||
import java.io.IOException
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import java.io.File
|
||||
import java.io.FileDescriptor
|
||||
import java.io.FileInputStream
|
||||
import java.lang.ref.SoftReference
|
||||
import android.os.Build
|
||||
import android.annotation.TargetApi
|
||||
import android.util.Log
|
||||
import go.Seq
|
||||
import org.jetbrains.anko.doAsync
|
||||
|
||||
class V2RayVpnService : VpnService() {
|
||||
class V2RayVpnService : VpnService(), ServiceControl {
|
||||
companion object {
|
||||
const val NOTIFICATION_ID = 1
|
||||
const val NOTIFICATION_PENDING_INTENT_CONTENT = 0
|
||||
const val NOTIFICATION_PENDING_INTENT_STOP_V2RAY = 1
|
||||
|
||||
fun startV2Ray(context: Context) {
|
||||
val intent = Intent(context.applicationContext, V2RayVpnService::class.java)
|
||||
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.N_MR1) {
|
||||
context.startForegroundService(intent)
|
||||
} else {
|
||||
context.startService(intent)
|
||||
}
|
||||
}
|
||||
private const val VPN_MTU = 1500
|
||||
private const val PRIVATE_VLAN4_CLIENT = "26.26.26.1"
|
||||
private const val PRIVATE_VLAN4_ROUTER = "26.26.26.2"
|
||||
private const val PRIVATE_VLAN6_CLIENT = "da26:2626::1"
|
||||
private const val PRIVATE_VLAN6_ROUTER = "da26:2626::2"
|
||||
private const val TUN2SOCKS = "libtun2socks.so"
|
||||
}
|
||||
|
||||
private val v2rayPoint = Libv2ray.newV2RayPoint(V2RayCallback())
|
||||
private lateinit var configContent: String
|
||||
private val settingsStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_SETTING, MMKV.MULTI_PROCESS_MODE) }
|
||||
|
||||
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
|
||||
*
|
||||
* This makes doing a requestNetwork with REQUEST necessary so that we don't get ALL possible networks that
|
||||
@@ -74,55 +49,58 @@ class V2RayVpnService : VpnService() {
|
||||
*
|
||||
* Source: https://android.googlesource.com/platform/frameworks/base/+/2df4c7d/services/core/java/com/android/server/ConnectivityService.java#887
|
||||
*/
|
||||
@TargetApi(28)
|
||||
private val defaultNetworkRequest = NetworkRequest.Builder()
|
||||
@delegate:RequiresApi(Build.VERSION_CODES.P)
|
||||
private val defaultNetworkRequest by lazy {
|
||||
NetworkRequest.Builder()
|
||||
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
|
||||
.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED)
|
||||
.build()
|
||||
|
||||
}
|
||||
|
||||
private val connectivity by lazy { getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager }
|
||||
@TargetApi(28)
|
||||
private val defaultNetworkCallback = object : ConnectivityManager.NetworkCallback() {
|
||||
|
||||
@delegate:RequiresApi(Build.VERSION_CODES.P)
|
||||
private val defaultNetworkCallback by lazy {
|
||||
object : ConnectivityManager.NetworkCallback() {
|
||||
override fun onAvailable(network: Network) {
|
||||
setUnderlyingNetworks(arrayOf(network))
|
||||
}
|
||||
override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities?) {
|
||||
|
||||
override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) {
|
||||
// it's a good idea to refresh capabilities
|
||||
setUnderlyingNetworks(arrayOf(network))
|
||||
}
|
||||
|
||||
override fun onLost(network: Network) {
|
||||
setUnderlyingNetworks(null)
|
||||
}
|
||||
}
|
||||
private var listeningForDefaultNetwork = false
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
|
||||
val policy = StrictMode.ThreadPolicy.Builder().permitAll().build()
|
||||
StrictMode.setThreadPolicy(policy)
|
||||
v2rayPoint.packageName = Utils.packagePath(applicationContext)
|
||||
Seq.setContext(applicationContext)
|
||||
V2RayServiceManager.serviceControl = SoftReference(this)
|
||||
}
|
||||
|
||||
override fun onRevoke() {
|
||||
stopV2Ray()
|
||||
}
|
||||
|
||||
override fun onLowMemory() {
|
||||
stopV2Ray()
|
||||
super.onLowMemory()
|
||||
}
|
||||
// override fun onLowMemory() {
|
||||
// stopV2Ray()
|
||||
// super.onLowMemory()
|
||||
// }
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
cancelNotification()
|
||||
V2RayServiceManager.cancelNotification()
|
||||
}
|
||||
|
||||
fun setup(parameters: String) {
|
||||
|
||||
val prepare = VpnService.prepare(this)
|
||||
private fun setup() {
|
||||
val prepare = prepare(this)
|
||||
if (prepare != null) {
|
||||
return
|
||||
}
|
||||
@@ -130,33 +108,47 @@ class V2RayVpnService : VpnService() {
|
||||
// If the old interface has exactly the same parameters, use it!
|
||||
// Configure a builder while parsing the parameters.
|
||||
val builder = Builder()
|
||||
val enableLocalDns = defaultDPreference.getPrefBoolean(SettingsActivity.PREF_LOCAL_DNS_ENABLED, false)
|
||||
//val enableLocalDns = defaultDPreference.getPrefBoolean(AppConfig.PREF_LOCAL_DNS_ENABLED, false)
|
||||
|
||||
parameters.split(" ")
|
||||
.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])
|
||||
val routingMode = settingsStorage?.decodeString(AppConfig.PREF_ROUTING_MODE) ?: ERoutingMode.GLOBAL_PROXY.value
|
||||
|
||||
builder.setMtu(VPN_MTU)
|
||||
builder.addAddress(PRIVATE_VLAN4_CLIENT, 30)
|
||||
//builder.addDnsServer(PRIVATE_VLAN4_ROUTER)
|
||||
if (routingMode == ERoutingMode.BYPASS_LAN.value || routingMode == ERoutingMode.BYPASS_LAN_MAINLAND.value) {
|
||||
resources.getStringArray(R.array.bypass_private_ip_address).forEach {
|
||||
val addr = it.split('/')
|
||||
builder.addRoute(addr[0], addr[1].toInt())
|
||||
}
|
||||
} else {
|
||||
builder.addRoute("0.0.0.0", 0)
|
||||
}
|
||||
|
||||
if (settingsStorage?.decodeBool(AppConfig.PREF_PREFER_IPV6) == true) {
|
||||
builder.addAddress(PRIVATE_VLAN6_CLIENT, 126)
|
||||
if (routingMode == ERoutingMode.BYPASS_LAN.value || routingMode == ERoutingMode.BYPASS_LAN_MAINLAND.value) {
|
||||
builder.addRoute("2000::", 3) //currently only 1/8 of total ipV6 is in use
|
||||
} else {
|
||||
builder.addRoute("::", 0)
|
||||
}
|
||||
}
|
||||
|
||||
if(!enableLocalDns) {
|
||||
Utils.getRemoteDnsServers(defaultDPreference)
|
||||
if (settingsStorage?.decodeBool(AppConfig.PREF_LOCAL_DNS_ENABLED) == true) {
|
||||
builder.addDnsServer(PRIVATE_VLAN4_ROUTER)
|
||||
} else {
|
||||
Utils.getVpnDnsServers()
|
||||
.forEach {
|
||||
if (Utils.isPureIpAddress(it)) {
|
||||
builder.addDnsServer(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
builder.setSession(defaultDPreference.getPrefString(AppConfig.PREF_CURR_CONFIG_NAME, ""))
|
||||
builder.setSession(V2RayServiceManager.currentConfig?.remarks.orEmpty())
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP &&
|
||||
defaultDPreference.getPrefBoolean(SettingsActivity.PREF_PER_APP_PROXY, false)) {
|
||||
val apps = defaultDPreference.getPrefStringSet(PerAppProxyActivity.PREF_PER_APP_PROXY_SET, null)
|
||||
val bypassApps = defaultDPreference.getPrefBoolean(PerAppProxyActivity.PREF_BYPASS_APPS, false)
|
||||
if (settingsStorage?.decodeBool(AppConfig.PREF_PER_APP_PROXY) == true) {
|
||||
val apps = settingsStorage?.decodeStringSet(AppConfig.PREF_PER_APP_PROXY_SET)
|
||||
val bypassApps = settingsStorage?.decodeBool(AppConfig.PREF_BYPASS_APPS) ?: false
|
||||
apps?.forEach {
|
||||
try {
|
||||
if (bypassApps)
|
||||
@@ -173,33 +165,78 @@ class V2RayVpnService : VpnService() {
|
||||
try {
|
||||
mInterface.close()
|
||||
} catch (ignored: Exception) {
|
||||
// ignored
|
||||
}
|
||||
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 28) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
try {
|
||||
connectivity.requestNetwork(defaultNetworkRequest, defaultNetworkCallback)
|
||||
listeningForDefaultNetwork = true
|
||||
} 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.
|
||||
mInterface = builder.establish()
|
||||
try {
|
||||
mInterface = builder.establish()!!
|
||||
runTun2socks()
|
||||
} catch (e: Exception) {
|
||||
// non-nullable lateinit var
|
||||
e.printStackTrace()
|
||||
stopV2Ray()
|
||||
}
|
||||
}
|
||||
|
||||
private fun runTun2socks() {
|
||||
val socksPort = Utils.parseInt(settingsStorage?.decodeString(AppConfig.PREF_SOCKS_PORT), AppConfig.PORT_SOCKS.toInt())
|
||||
val cmd = arrayListOf(File(applicationContext.applicationInfo.nativeLibraryDir, TUN2SOCKS).absolutePath,
|
||||
"--netif-ipaddr", PRIVATE_VLAN4_ROUTER,
|
||||
"--netif-netmask", "255.255.255.252",
|
||||
"--socks-server-addr", "127.0.0.1:${socksPort}",
|
||||
"--tunmtu", VPN_MTU.toString(),
|
||||
"--sock-path", "sock_path",//File(applicationContext.filesDir, "sock_path").absolutePath,
|
||||
"--enable-udprelay",
|
||||
"--loglevel", "notice")
|
||||
|
||||
if (settingsStorage?.decodeBool(AppConfig.PREF_PREFER_IPV6) == true) {
|
||||
cmd.add("--netif-ip6addr")
|
||||
cmd.add(PRIVATE_VLAN6_ROUTER)
|
||||
}
|
||||
if (settingsStorage?.decodeBool(AppConfig.PREF_LOCAL_DNS_ENABLED) == true) {
|
||||
val localDnsPort = Utils.parseInt(settingsStorage?.decodeString(AppConfig.PREF_LOCAL_DNS_PORT), AppConfig.PORT_LOCAL_DNS.toInt())
|
||||
cmd.add("--dnsgw")
|
||||
cmd.add("127.0.0.1:${localDnsPort}")
|
||||
}
|
||||
Log.d(packageName, cmd.toString())
|
||||
|
||||
try {
|
||||
val proBuilder = ProcessBuilder(cmd)
|
||||
proBuilder.redirectErrorStream(true)
|
||||
process = proBuilder
|
||||
.directory(applicationContext.filesDir)
|
||||
.start()
|
||||
Log.d(packageName, process.toString())
|
||||
|
||||
sendFd()
|
||||
startSpeedNotification()
|
||||
} catch (e: Exception) {
|
||||
Log.d(packageName, e.toString())
|
||||
}
|
||||
}
|
||||
|
||||
fun shutdown() {
|
||||
stopV2Ray(true)
|
||||
}
|
||||
|
||||
fun sendFd() {
|
||||
private fun sendFd() {
|
||||
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
|
||||
while (true) try {
|
||||
Thread.sleep(50L shl tries)
|
||||
Log.d(packageName, "sendFd tries: " + tries.toString())
|
||||
Log.d(packageName, "sendFd tries: $tries")
|
||||
LocalSocket().use { localSocket ->
|
||||
localSocket.connect(LocalSocketAddress(path, LocalSocketAddress.Namespace.FILESYSTEM))
|
||||
localSocket.setFileDescriptorsForSend(arrayOf(fd))
|
||||
@@ -215,74 +252,34 @@ class V2RayVpnService : VpnService() {
|
||||
}
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
startV2ray()
|
||||
V2RayServiceManager.startV2rayPoint()
|
||||
return START_STICKY
|
||||
//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) {
|
||||
// val configName = defaultDPreference.getPrefString(PREF_CURR_CONFIG_GUID, "")
|
||||
// val emptyInfo = VpnNetworkInfo()
|
||||
// val info = loadVpnNetworkInfo(configName, emptyInfo)!! + (lastNetworkInfo ?: emptyInfo)
|
||||
// saveVpnNetworkInfo(configName, info)
|
||||
if (Build.VERSION.SDK_INT >= 28) {
|
||||
if (listeningForDefaultNetwork) {
|
||||
connectivity.unregisterNetworkCallback(defaultNetworkCallback)
|
||||
listeningForDefaultNetwork = false
|
||||
}
|
||||
}
|
||||
if (v2rayPoint.isRunning) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
try {
|
||||
v2rayPoint.stopLoop()
|
||||
connectivity.unregisterNetworkCallback(defaultNetworkCallback)
|
||||
} catch (ignored: Exception) {
|
||||
// ignored
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
Log.d(packageName, "tun2socks destroy")
|
||||
process.destroy()
|
||||
} catch (e: Exception) {
|
||||
Log.d(packageName, e.toString())
|
||||
}
|
||||
}
|
||||
|
||||
MessageUtil.sendMsg2UI(this, AppConfig.MSG_STATE_STOP_SUCCESS, "")
|
||||
cancelNotification()
|
||||
V2RayServiceManager.stopV2rayPoint()
|
||||
|
||||
if (isForced) {
|
||||
try {
|
||||
unregisterReceiver(mMsgReceive)
|
||||
} catch (e: Exception) {
|
||||
}
|
||||
|
||||
//stopSelf has to be called ahead of mInterface.close(). otherwise v2ray core cannot be stooped
|
||||
//It's strage but true.
|
||||
//This can be verified by putting stopself() behind and call stopLoop and startLoop
|
||||
@@ -293,207 +290,32 @@ class V2RayVpnService : VpnService() {
|
||||
try {
|
||||
mInterface.close()
|
||||
} catch (ignored: Exception) {
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
// ignored
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun stopSpeedNotification() {
|
||||
if (mSubscription != null) {
|
||||
mSubscription?.unsubscribe() //stop queryStats
|
||||
mSubscription = null
|
||||
|
||||
val cf_name = defaultDPreference.getPrefString(AppConfig.PREF_CURR_CONFIG_NAME, "")
|
||||
updateNotification(cf_name)
|
||||
}
|
||||
override fun getService(): Service {
|
||||
return this
|
||||
}
|
||||
|
||||
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 startService() {
|
||||
setup()
|
||||
}
|
||||
|
||||
override fun prepare(): Long {
|
||||
return 0
|
||||
override fun stopService() {
|
||||
stopV2Ray(true)
|
||||
}
|
||||
|
||||
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 vpnProtect(socket: Int): Boolean {
|
||||
return protect(socket)
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
private class ReceiveMessageHandler(vpnService: V2RayVpnService) : BroadcastReceiver() {
|
||||
internal var mReference: SoftReference<V2RayVpnService> = SoftReference(vpnService)
|
||||
|
||||
override fun onReceive(ctx: Context?, intent: Intent?) {
|
||||
val vpnService = mReference.get()
|
||||
when (intent?.getIntExtra("key", 0)) {
|
||||
AppConfig.MSG_REGISTER_CLIENT -> {
|
||||
//Logger.e("ReceiveMessageHandler", intent?.getIntExtra("key", 0).toString())
|
||||
|
||||
val isRunning = vpnService?.v2rayPoint!!.isRunning
|
||||
&& VpnService.prepare(vpnService) == null
|
||||
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) {
|
||||
Intent.ACTION_SCREEN_OFF -> {
|
||||
Log.d(AppConfig.ANG_PACKAGE, "SCREEN_OFF, stop querying stats")
|
||||
vpnService?.stopSpeedNotification()
|
||||
}
|
||||
Intent.ACTION_SCREEN_ON -> {
|
||||
Log.d(AppConfig.ANG_PACKAGE, "SCREEN_ON, start querying stats")
|
||||
vpnService?.startSpeedNotification()
|
||||
}
|
||||
}
|
||||
@RequiresApi(Build.VERSION_CODES.N)
|
||||
override fun attachBaseContext(newBase: Context?) {
|
||||
val context = newBase?.let {
|
||||
MyContextWrapper.wrap(newBase, Utils.getLocale(newBase))
|
||||
}
|
||||
super.attachBaseContext(context)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
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 androidx.annotation.RequiresApi
|
||||
import com.v2ray.ang.util.MyContextWrapper
|
||||
import com.v2ray.ang.R
|
||||
import com.v2ray.ang.util.Utils
|
||||
|
||||
abstract class BaseActivity : AppCompatActivity() {
|
||||
override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
|
||||
@@ -11,4 +18,36 @@ abstract class BaseActivity : AppCompatActivity() {
|
||||
}
|
||||
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
|
||||
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import androidx.viewpager2.adapter.FragmentStateAdapter
|
||||
|
||||
import android.support.v4.app.Fragment
|
||||
import android.support.v4.app.FragmentManager
|
||||
import android.support.v4.app.FragmentStatePagerAdapter
|
||||
class FragmentAdapter(fragmentActivity: FragmentActivity, private val mFragments: List<Fragment>) :
|
||||
FragmentStateAdapter(fragmentActivity) {
|
||||
|
||||
class FragmentAdapter(fm: FragmentManager, private val mFragments: List<Fragment>, private val mTitles: List<String>) : FragmentStatePagerAdapter(fm) {
|
||||
|
||||
override fun getItem(position: Int): Fragment {
|
||||
override fun createFragment(position: Int): Fragment {
|
||||
return mFragments[position]
|
||||
}
|
||||
|
||||
override fun getCount(): Int {
|
||||
override fun getItemCount(): Int {
|
||||
return mFragments.size
|
||||
}
|
||||
|
||||
override fun getPageTitle(position: Int): CharSequence? {
|
||||
return mTitles[position]
|
||||
}
|
||||
}
|
||||
@@ -1,72 +1,92 @@
|
||||
package com.v2ray.ang.ui
|
||||
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.os.Bundle
|
||||
import android.text.method.ScrollingMovementMethod
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.v2ray.ang.AppConfig.ANG_PACKAGE
|
||||
import com.v2ray.ang.R
|
||||
import com.v2ray.ang.databinding.ActivityLogcatBinding
|
||||
import com.v2ray.ang.extension.toast
|
||||
import com.v2ray.ang.util.Utils
|
||||
import kotlinx.android.synthetic.main.activity_logcat.*
|
||||
import org.jetbrains.anko.doAsync
|
||||
import org.jetbrains.anko.toast
|
||||
import org.jetbrains.anko.uiThread
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
import java.io.IOException
|
||||
import java.util.LinkedHashSet
|
||||
|
||||
class LogcatActivity : BaseActivity() {
|
||||
private lateinit var binding: ActivityLogcatBinding
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_logcat)
|
||||
binding = ActivityLogcatBinding.inflate(layoutInflater)
|
||||
val view = binding.root
|
||||
setContentView(view)
|
||||
|
||||
title = getString(R.string.title_logcat)
|
||||
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
logcat()
|
||||
logcat(false)
|
||||
}
|
||||
|
||||
private fun logcat() {
|
||||
private fun logcat(shouldFlushLog: Boolean) {
|
||||
|
||||
try {
|
||||
pb_waiting.visibility = View.VISIBLE
|
||||
binding.pbWaiting.visibility = View.VISIBLE
|
||||
|
||||
doAsync {
|
||||
lifecycleScope.launch(Dispatchers.Default) {
|
||||
if (shouldFlushLog) {
|
||||
val lst = LinkedHashSet<String>()
|
||||
lst.add("logcat")
|
||||
lst.add("-c")
|
||||
val process = Runtime.getRuntime().exec(lst.toTypedArray())
|
||||
process.waitFor()
|
||||
}
|
||||
val lst = LinkedHashSet<String>()
|
||||
lst.add("logcat")
|
||||
lst.add("-d")
|
||||
lst.add("-v")
|
||||
lst.add("time")
|
||||
lst.add("-s")
|
||||
lst.add("GoLog,tun2socks,com.v2ray.ang")
|
||||
lst.add("GoLog,tun2socks,${ANG_PACKAGE},AndroidRuntime,System.err")
|
||||
val process = Runtime.getRuntime().exec(lst.toTypedArray())
|
||||
// val bufferedReader = BufferedReader(
|
||||
// InputStreamReader(process.inputStream))
|
||||
// val allText = bufferedReader.use(BufferedReader::readText)
|
||||
val allText = process.inputStream.bufferedReader().use { it.readText() }
|
||||
uiThread {
|
||||
tv_logcat.text = allText
|
||||
tv_logcat.movementMethod = ScrollingMovementMethod()
|
||||
pb_waiting.visibility = View.GONE
|
||||
launch(Dispatchers.Main) {
|
||||
binding.tvLogcat.text = allText
|
||||
binding.tvLogcat.movementMethod = ScrollingMovementMethod()
|
||||
binding.pbWaiting.visibility = View.GONE
|
||||
Handler(Looper.getMainLooper()).post { binding.svLogcat.fullScroll(View.FOCUS_DOWN) }
|
||||
}
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
|
||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||
menuInflater.inflate(R.menu.menu_logcat, menu)
|
||||
return super.onCreateOptionsMenu(menu)
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
|
||||
R.id.copy_all -> {
|
||||
Utils.setClipboard(this, tv_logcat.text.toString())
|
||||
Utils.setClipboard(this, binding.tvLogcat.text.toString())
|
||||
toast(R.string.toast_success)
|
||||
true
|
||||
}
|
||||
|
||||
R.id.clear_all -> {
|
||||
logcat(true)
|
||||
true
|
||||
}
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,184 +4,213 @@ import android.Manifest
|
||||
import android.content.*
|
||||
import android.net.Uri
|
||||
import android.net.VpnService
|
||||
import android.support.v7.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import com.tbruyelle.rxpermissions.RxPermissions
|
||||
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.text.TextUtils
|
||||
import android.view.KeyEvent
|
||||
import com.v2ray.ang.AppConfig
|
||||
import com.v2ray.ang.util.MessageUtil
|
||||
import com.v2ray.ang.util.V2rayConfigUtil
|
||||
import org.jetbrains.anko.*
|
||||
import java.lang.ref.SoftReference
|
||||
import java.net.URL
|
||||
import android.content.IntentFilter
|
||||
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.content.res.ColorStateList
|
||||
import com.google.android.material.navigation.NavigationView
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.GravityCompat
|
||||
import androidx.appcompat.app.ActionBarDrawerToggle
|
||||
import androidx.recyclerview.widget.ItemTouchHelper
|
||||
import android.util.Log
|
||||
//import 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.android.schedulers.AndroidSchedulers
|
||||
import java.util.concurrent.TimeUnit
|
||||
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 {
|
||||
companion object {
|
||||
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 lateinit var binding: ActivityMainBinding
|
||||
|
||||
private val adapter by lazy { MainRecyclerAdapter(this) }
|
||||
private val mainStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_MAIN, MMKV.MULTI_PROCESS_MODE) }
|
||||
private val settingsStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_SETTING, MMKV.MULTI_PROCESS_MODE) }
|
||||
private val requestVpnPermission = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||
if (it.resultCode == RESULT_OK) {
|
||||
startV2Ray()
|
||||
}
|
||||
}
|
||||
private var mItemTouchHelper: ItemTouchHelper? = null
|
||||
val mainViewModel: MainViewModel by viewModels()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_main)
|
||||
binding = ActivityMainBinding.inflate(layoutInflater)
|
||||
val view = binding.root
|
||||
setContentView(view)
|
||||
title = getString(R.string.title_server)
|
||||
setSupportActionBar(toolbar)
|
||||
setSupportActionBar(binding.toolbar)
|
||||
|
||||
fab.setOnClickListener {
|
||||
if (isRunning) {
|
||||
binding.fab.setOnClickListener {
|
||||
if (mainViewModel.isRunning.value == true) {
|
||||
Utils.stopVService(this)
|
||||
} else {
|
||||
} else if (settingsStorage?.decodeString(AppConfig.PREF_MODE) ?: "VPN" == "VPN") {
|
||||
val intent = VpnService.prepare(this)
|
||||
if (intent == null) {
|
||||
startV2Ray()
|
||||
} else {
|
||||
startActivityForResult(intent, REQUEST_CODE_VPN_PREPARE)
|
||||
}
|
||||
}
|
||||
}
|
||||
layout_test.setOnClickListener {
|
||||
if (isRunning) {
|
||||
val socksPort = 10808//Utils.parseInt(defaultDPreference.getPrefString(SettingsActivity.PREF_SOCKS_PORT, "10808"))
|
||||
|
||||
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)
|
||||
requestVpnPermission.launch(intent)
|
||||
}
|
||||
} else {
|
||||
startV2Ray()
|
||||
}
|
||||
}
|
||||
binding.layoutTest.setOnClickListener {
|
||||
if (mainViewModel.isRunning.value == true) {
|
||||
setTestState(getString(R.string.connection_test_testing))
|
||||
mainViewModel.testCurrentServerRealPing()
|
||||
} else {
|
||||
// tv_test_state.text = getString(R.string.connection_test_fail)
|
||||
}
|
||||
}
|
||||
|
||||
recycler_view.setHasFixedSize(true)
|
||||
recycler_view.layoutManager = LinearLayoutManager(this)
|
||||
recycler_view.adapter = adapter
|
||||
binding.recyclerView.setHasFixedSize(true)
|
||||
binding.recyclerView.layoutManager = LinearLayoutManager(this)
|
||||
binding.recyclerView.adapter = adapter
|
||||
|
||||
val callback = SimpleItemTouchHelperCallback(adapter)
|
||||
mItemTouchHelper = ItemTouchHelper(callback)
|
||||
mItemTouchHelper?.attachToRecyclerView(recycler_view)
|
||||
mItemTouchHelper?.attachToRecyclerView(binding.recyclerView)
|
||||
|
||||
|
||||
val toggle = ActionBarDrawerToggle(
|
||||
this, drawer_layout, toolbar, R.string.navigation_drawer_open, R.string.navigation_drawer_close)
|
||||
drawer_layout.addDrawerListener(toggle)
|
||||
this, binding.drawerLayout, binding.toolbar, R.string.navigation_drawer_open, R.string.navigation_drawer_close)
|
||||
binding.drawerLayout.addDrawerListener(toggle)
|
||||
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() {
|
||||
if (AngConfigManager.configs.index < 0) {
|
||||
if (mainStorage?.decodeString(MmkvManager.KEY_SELECTED_SERVER).isNullOrEmpty()) {
|
||||
return
|
||||
}
|
||||
showCircle()
|
||||
// toast(R.string.toast_services_start)
|
||||
if (!Utils.startVService(this)) {
|
||||
V2RayServiceManager.startV2Ray(this)
|
||||
hideCircle()
|
||||
}
|
||||
|
||||
fun restartV2Ray() {
|
||||
if (mainViewModel.isRunning.value == true) {
|
||||
Utils.stopVService(this)
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
isRunning = false
|
||||
|
||||
// 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() {
|
||||
super.onResume()
|
||||
adapter.updateConfigList()
|
||||
mainViewModel.reloadServerList()
|
||||
}
|
||||
|
||||
public override fun onPause() {
|
||||
super.onPause()
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
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 {
|
||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||
menuInflater.inflate(R.menu.menu_main, menu)
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
|
||||
R.id.import_qrcode -> {
|
||||
importQRcode(REQUEST_SCAN)
|
||||
importQRcode(true)
|
||||
true
|
||||
}
|
||||
R.id.import_clipboard -> {
|
||||
@@ -189,18 +218,23 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
||||
true
|
||||
}
|
||||
R.id.import_manually_vmess -> {
|
||||
startActivity<ServerActivity>("position" to -1, "isRunning" to isRunning)
|
||||
adapter.updateConfigList()
|
||||
importManually(EConfigType.VMESS.value)
|
||||
true
|
||||
}
|
||||
R.id.import_manually_vless -> {
|
||||
importManually(EConfigType.VLESS.value)
|
||||
true
|
||||
}
|
||||
R.id.import_manually_ss -> {
|
||||
startActivity<Server3Activity>("position" to -1, "isRunning" to isRunning)
|
||||
adapter.updateConfigList()
|
||||
importManually(EConfigType.SHADOWSOCKS.value)
|
||||
true
|
||||
}
|
||||
R.id.import_manually_socks -> {
|
||||
startActivity<Server4Activity>("position" to -1, "isRunning" to isRunning)
|
||||
adapter.updateConfigList()
|
||||
importManually(EConfigType.SOCKS.value)
|
||||
true
|
||||
}
|
||||
R.id.import_manually_trojan -> {
|
||||
importManually(EConfigType.TROJAN.value)
|
||||
true
|
||||
}
|
||||
R.id.import_config_custom_clipboard -> {
|
||||
@@ -216,7 +250,7 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
||||
true
|
||||
}
|
||||
R.id.import_config_custom_url_scan -> {
|
||||
importQRcode(REQUEST_SCAN_URL)
|
||||
importQRcode(false)
|
||||
true
|
||||
}
|
||||
|
||||
@@ -231,8 +265,8 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
||||
}
|
||||
|
||||
R.id.export_all -> {
|
||||
if (AngConfigManager.shareAll2Clipboard() == 0) {
|
||||
//remove toast, otherwise it will block previous warning message
|
||||
if (AngConfigManager.shareNonCustomConfigsToClipboard(this, mainViewModel.serverList) == 0) {
|
||||
toast(R.string.toast_success)
|
||||
} else {
|
||||
toast(R.string.toast_failure)
|
||||
}
|
||||
@@ -240,39 +274,65 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
||||
}
|
||||
|
||||
R.id.ping_all -> {
|
||||
for (k in 0 until configs.vmess.count()) {
|
||||
configs.vmess[k].testResult = ""
|
||||
adapter.updateConfigList()
|
||||
}
|
||||
for (k in 0 until configs.vmess.count()) {
|
||||
if (configs.vmess[k].configType != AppConfig.EConfigType.Custom) {
|
||||
doAsync {
|
||||
configs.vmess[k].testResult = Utils.tcping(configs.vmess[k].address, configs.vmess[k].port)
|
||||
uiThread {
|
||||
adapter.updateSelectedItem(k)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
mainViewModel.testAllTcping()
|
||||
true
|
||||
}
|
||||
|
||||
R.id.real_ping_all -> {
|
||||
mainViewModel.testAllRealPing()
|
||||
true
|
||||
}
|
||||
|
||||
R.id.service_restart -> {
|
||||
restartV2Ray()
|
||||
true
|
||||
}
|
||||
|
||||
R.id.del_all_config -> {
|
||||
AlertDialog.Builder(this).setMessage(R.string.del_config_comfirm)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
MmkvManager.removeAllServer()
|
||||
mainViewModel.reloadServerList()
|
||||
}
|
||||
.show()
|
||||
true
|
||||
}
|
||||
|
||||
R.id.del_invalid_config -> {
|
||||
AlertDialog.Builder(this).setMessage(R.string.del_config_comfirm)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
MmkvManager.removeInvalidServer()
|
||||
mainViewModel.reloadServerList()
|
||||
}
|
||||
.show()
|
||||
true
|
||||
}
|
||||
R.id.sort_by_test_results -> {
|
||||
MmkvManager.sortByTestResults()
|
||||
mainViewModel.reloadServerList()
|
||||
true
|
||||
}
|
||||
R.id.filter_config -> {
|
||||
mainViewModel.filterConfig(this)
|
||||
true
|
||||
}
|
||||
|
||||
// R.id.settings -> {
|
||||
// startActivity<SettingsActivity>("isRunning" to isRunning)
|
||||
// true
|
||||
// }
|
||||
// R.id.logcat -> {
|
||||
// startActivity<LogcatActivity>()
|
||||
// true
|
||||
// }
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
private fun importManually(createConfigType : Int) {
|
||||
startActivity(
|
||||
Intent()
|
||||
.putExtra("createConfigType", createConfigType)
|
||||
.putExtra("subscriptionId", mainViewModel.subscriptionId)
|
||||
.setClass(this, ServerActivity::class.java)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* import config from qrcode
|
||||
*/
|
||||
fun importQRcode(requestCode: Int): Boolean {
|
||||
fun importQRcode(forConfig: Boolean): Boolean {
|
||||
// try {
|
||||
// startActivityForResult(Intent("com.google.zxing.client.android.SCAN")
|
||||
// .addCategory(Intent.CATEGORY_DEFAULT)
|
||||
@@ -282,7 +342,10 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
||||
.request(Manifest.permission.CAMERA)
|
||||
.subscribe {
|
||||
if (it)
|
||||
startActivityForResult<ScannerActivity>(requestCode)
|
||||
if (forConfig)
|
||||
scanQRCodeForConfig.launch(Intent(this, ScannerActivity::class.java))
|
||||
else
|
||||
scanQRCodeForUrlToCustomConfig.launch(Intent(this, ScannerActivity::class.java))
|
||||
else
|
||||
toast(R.string.toast_permission_denied)
|
||||
}
|
||||
@@ -290,6 +353,18 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
||||
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
|
||||
*/
|
||||
@@ -306,10 +381,20 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
||||
}
|
||||
|
||||
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) {
|
||||
toast(R.string.toast_success)
|
||||
adapter.updateConfigList()
|
||||
mainViewModel.reloadServerList()
|
||||
} else {
|
||||
toast(R.string.toast_failure)
|
||||
}
|
||||
@@ -368,9 +453,14 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
||||
toast(R.string.toast_invalid_url)
|
||||
return false
|
||||
}
|
||||
doAsync {
|
||||
val configText = URL(url).readText()
|
||||
uiThread {
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
val configText = try {
|
||||
Utils.getUrlContentWithCustomUserAgent(url)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
""
|
||||
}
|
||||
launch(Dispatchers.Main) {
|
||||
importCustomizeConfig(configText)
|
||||
}
|
||||
}
|
||||
@@ -388,24 +478,33 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
||||
: Boolean {
|
||||
try {
|
||||
toast(R.string.title_sub_update)
|
||||
val subItem = AngConfigManager.configs.subItem
|
||||
for (k in 0 until subItem.count()) {
|
||||
if (TextUtils.isEmpty(subItem[k].id)
|
||||
|| TextUtils.isEmpty(subItem[k].remarks)
|
||||
|| TextUtils.isEmpty(subItem[k].url)
|
||||
MmkvManager.decodeSubscriptions().forEach {
|
||||
if (TextUtils.isEmpty(it.first)
|
||||
|| TextUtils.isEmpty(it.second.remarks)
|
||||
|| TextUtils.isEmpty(it.second.url)
|
||||
) {
|
||||
continue
|
||||
return@forEach
|
||||
}
|
||||
val id = subItem[k].id
|
||||
val url = subItem[k].url
|
||||
if (!it.second.enabled) {
|
||||
return@forEach
|
||||
}
|
||||
val url = it.second.url
|
||||
if (!Utils.isValidUrl(url)) {
|
||||
continue
|
||||
return@forEach
|
||||
}
|
||||
Log.d("Main", url)
|
||||
doAsync {
|
||||
val configText = URL(url).readText()
|
||||
uiThread {
|
||||
importBatchConfig(Utils.decode(configText), id)
|
||||
Log.d(ANG_PACKAGE, url)
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
val configText = try {
|
||||
Utils.getUrlContentWithCustomUserAgent(url)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
launch(Dispatchers.Main) {
|
||||
toast("\"" + it.second.remarks + "\" " + getString(R.string.toast_failure))
|
||||
}
|
||||
return@launch
|
||||
}
|
||||
launch(Dispatchers.Main) {
|
||||
importBatchConfig(configText, it.first)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -425,14 +524,19 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
||||
intent.addCategory(Intent.CATEGORY_OPENABLE)
|
||||
|
||||
try {
|
||||
startActivityForResult(
|
||||
Intent.createChooser(intent, getString(R.string.title_file_chooser)),
|
||||
REQUEST_FILE_CHOOSER)
|
||||
} catch (ex: android.content.ActivityNotFoundException) {
|
||||
chooseFileForCustomConfig.launch(Intent.createChooser(intent, getString(R.string.title_file_chooser)))
|
||||
} catch (ex: ActivityNotFoundException) {
|
||||
toast(R.string.toast_require_file_manager)
|
||||
}
|
||||
}
|
||||
|
||||
private val chooseFileForCustomConfig = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||
val uri = it.data?.data
|
||||
if (it.resultCode == RESULT_OK && uri != null) {
|
||||
readContentFromUri(uri)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* read content from uri
|
||||
*/
|
||||
@@ -442,9 +546,9 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
||||
.subscribe {
|
||||
if (it) {
|
||||
try {
|
||||
val inputStream = contentResolver.openInputStream(uri)
|
||||
val configText = inputStream.bufferedReader().readText()
|
||||
importCustomizeConfig(configText)
|
||||
contentResolver.openInputStream(uri).use { input ->
|
||||
importCustomizeConfig(input?.bufferedReader()?.readText())
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
@@ -457,22 +561,26 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
||||
* import customize config
|
||||
*/
|
||||
fun importCustomizeConfig(server: String?) {
|
||||
if (server == null) {
|
||||
try {
|
||||
if (server == null || TextUtils.isEmpty(server)) {
|
||||
toast(R.string.toast_none_data)
|
||||
return
|
||||
}
|
||||
if (!V2rayConfigUtil.isValidConfig(server)) {
|
||||
toast(R.string.toast_config_file_invalid)
|
||||
return
|
||||
}
|
||||
val resId = AngConfigManager.importCustomizeConfig(server)
|
||||
if (resId > 0) {
|
||||
toast(resId)
|
||||
} else {
|
||||
mainViewModel.appendCustomConfigServer(server)
|
||||
mainViewModel.reloadServerList()
|
||||
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 {
|
||||
// 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 {
|
||||
if (keyCode == KeyEvent.KEYCODE_BACK) {
|
||||
moveTaskToBack(false)
|
||||
@@ -520,7 +599,7 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
||||
}
|
||||
|
||||
fun showCircle() {
|
||||
fabProgressCircle?.show()
|
||||
binding.fabProgressCircle.show()
|
||||
}
|
||||
|
||||
fun hideCircle() {
|
||||
@@ -528,17 +607,22 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
||||
Observable.timer(300, TimeUnit.MILLISECONDS)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe {
|
||||
if (fabProgressCircle.isShown) {
|
||||
fabProgressCircle.hide()
|
||||
try {
|
||||
if (binding.fabProgressCircle.isShown) {
|
||||
binding.fabProgressCircle.hide()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.w(ANG_PACKAGE, e)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.d(ANG_PACKAGE, e.toString())
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBackPressed() {
|
||||
if (drawer_layout.isDrawerOpen(GravityCompat.START)) {
|
||||
drawer_layout.closeDrawer(GravityCompat.START)
|
||||
if (binding.drawerLayout.isDrawerOpen(GravityCompat.START)) {
|
||||
binding.drawerLayout.closeDrawer(GravityCompat.START)
|
||||
} else {
|
||||
super.onBackPressed()
|
||||
}
|
||||
@@ -549,25 +633,26 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
||||
when (item.itemId) {
|
||||
//R.id.server_profile -> activityClass = MainActivity::class.java
|
||||
R.id.sub_setting -> {
|
||||
startActivity<SubSettingActivity>()
|
||||
startActivity(Intent(this, SubSettingActivity::class.java))
|
||||
}
|
||||
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 -> {
|
||||
Utils.openUri(this, AppConfig.v2rayNGIssues)
|
||||
}
|
||||
R.id.promotion -> {
|
||||
Utils.openUri(this, AppConfig.promotionUrl)
|
||||
}
|
||||
R.id.donate -> {
|
||||
// startActivity<InappBuyActivity>()
|
||||
Utils.openUri(this, "${Utils.decode(AppConfig.promotionUrl)}?t=${System.currentTimeMillis()}")
|
||||
}
|
||||
R.id.logcat -> {
|
||||
startActivity<LogcatActivity>()
|
||||
startActivity(Intent(this, LogcatActivity::class.java))
|
||||
}
|
||||
}
|
||||
drawer_layout.closeDrawer(GravityCompat.START)
|
||||
binding.drawerLayout.closeDrawer(GravityCompat.START)
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -1,24 +1,33 @@
|
||||
package com.v2ray.ang.ui
|
||||
|
||||
import android.content.Intent
|
||||
import android.graphics.Color
|
||||
import android.support.v7.widget.RecyclerView
|
||||
import android.text.TextUtils
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import com.google.gson.Gson
|
||||
import com.tencent.mmkv.MMKV
|
||||
import com.v2ray.ang.AppConfig
|
||||
import com.v2ray.ang.R
|
||||
import com.v2ray.ang.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.ItemTouchHelperViewHolder
|
||||
import com.v2ray.ang.service.V2RayServiceManager
|
||||
import com.v2ray.ang.util.AngConfigManager
|
||||
import com.v2ray.ang.util.MmkvManager
|
||||
import com.v2ray.ang.util.Utils
|
||||
import 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.android.schedulers.AndroidSchedulers
|
||||
import java.util.concurrent.TimeUnit
|
||||
import com.v2ray.ang.extension.defaultDPreference
|
||||
|
||||
class MainRecyclerAdapter(val activity: MainActivity) : RecyclerView.Adapter<MainRecyclerAdapter.BaseViewHolder>()
|
||||
, ItemTouchHelperAdapter {
|
||||
@@ -28,241 +37,224 @@ class MainRecyclerAdapter(val activity: MainActivity) : RecyclerView.Adapter<Mai
|
||||
}
|
||||
|
||||
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 {
|
||||
mActivity.resources.getStringArray(R.array.share_method)
|
||||
}
|
||||
var isRunning = false
|
||||
|
||||
var changeable: Boolean = true
|
||||
set(value) {
|
||||
if (field == value)
|
||||
return
|
||||
field = value
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
init {
|
||||
updateConfigList()
|
||||
}
|
||||
|
||||
override fun getItemCount() = configs.vmess.count() + 1
|
||||
override fun getItemCount() = mActivity.mainViewModel.serversCache.size + 1
|
||||
|
||||
override fun onBindViewHolder(holder: BaseViewHolder, position: Int) {
|
||||
if (holder is MainViewHolder) {
|
||||
val configType = configs.vmess[position].configType
|
||||
val remarks = configs.vmess[position].remarks
|
||||
val subid = configs.vmess[position].subid
|
||||
val address = configs.vmess[position].address
|
||||
val port = configs.vmess[position].port
|
||||
val test_result = configs.vmess[position].testResult
|
||||
val guid = mActivity.mainViewModel.serversCache[position].guid
|
||||
val config = mActivity.mainViewModel.serversCache[position].config
|
||||
// //filter
|
||||
// if (mActivity.mainViewModel.subscriptionId.isNotEmpty()
|
||||
// && mActivity.mainViewModel.subscriptionId != config.subscriptionId
|
||||
// ) {
|
||||
// holder.itemMainBinding.cardView.visibility = View.GONE
|
||||
// } else {
|
||||
// holder.itemMainBinding.cardView.visibility = View.VISIBLE
|
||||
// }
|
||||
|
||||
holder.name.text = remarks
|
||||
holder.radio.isChecked = (position == configs.index)
|
||||
holder.itemView.backgroundColor = Color.TRANSPARENT
|
||||
holder.test_result.text = test_result
|
||||
val outbound = config.getProxyOutbound()
|
||||
val aff = MmkvManager.decodeServerAffiliationInfo(guid)
|
||||
|
||||
if (TextUtils.isEmpty(subid)) {
|
||||
holder.subid.text = ""
|
||||
holder.itemMainBinding.tvName.text = config.remarks
|
||||
holder.itemView.setBackgroundColor(Color.TRANSPARENT)
|
||||
holder.itemMainBinding.tvTestResult.text = aff?.getTestDelayString() ?: ""
|
||||
if ((aff?.testDelayMillis ?: 0L) < 0L) {
|
||||
holder.itemMainBinding.tvTestResult.setTextColor(ContextCompat.getColor(mActivity, R.color.colorPingRed))
|
||||
} else {
|
||||
holder.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) {
|
||||
holder.type.text = "vmess"
|
||||
holder.statistics.text = "$address : $port"
|
||||
holder.layout_share.visibility = View.VISIBLE
|
||||
} else if (configType == AppConfig.EConfigType.Custom) {
|
||||
holder.type.text = mActivity.getString(R.string.server_customize_config)
|
||||
holder.statistics.text = ""//mActivity.getString(R.string.server_customize_config)
|
||||
holder.layout_share.visibility = View.INVISIBLE
|
||||
} else if (configType == AppConfig.EConfigType.Shadowsocks) {
|
||||
holder.type.text = "shadowsocks"
|
||||
holder.statistics.text = "$address : $port"
|
||||
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
|
||||
var shareOptions = share_method.asList()
|
||||
when (config.configType) {
|
||||
EConfigType.CUSTOM -> {
|
||||
holder.itemMainBinding.tvType.text = mActivity.getString(R.string.server_customize_config)
|
||||
shareOptions = shareOptions.takeLast(1)
|
||||
}
|
||||
EConfigType.VLESS -> {
|
||||
holder.itemMainBinding.tvType.text = config.configType.name
|
||||
}
|
||||
else -> {
|
||||
holder.itemMainBinding.tvType.text = config.configType.name.lowercase()
|
||||
}
|
||||
}
|
||||
holder.itemMainBinding.tvStatistics.text = "${outbound?.getServerAddress()} : ${outbound?.getServerPort()}"
|
||||
|
||||
holder.layout_share.setOnClickListener {
|
||||
mActivity.selector(null, share_method.asList()) { dialogInterface, i ->
|
||||
holder.itemMainBinding.layoutShare.setOnClickListener {
|
||||
AlertDialog.Builder(mActivity).setItems(shareOptions.toTypedArray()) { _, i ->
|
||||
try {
|
||||
when (i) {
|
||||
0 -> {
|
||||
val iv = mActivity.layoutInflater.inflate(R.layout.item_qrcode, null)
|
||||
iv.iv_qcode.setImageBitmap(AngConfigManager.share2QRCode(position))
|
||||
|
||||
mActivity.alert {
|
||||
customView {
|
||||
linearLayout {
|
||||
addView(iv)
|
||||
if (config.configType == EConfigType.CUSTOM) {
|
||||
shareFullContent(guid)
|
||||
} else {
|
||||
val ivBinding = ItemQrcodeBinding.inflate(LayoutInflater.from(mActivity))
|
||||
ivBinding.ivQcode.setImageBitmap(AngConfigManager.share2QRCode(guid))
|
||||
AlertDialog.Builder(mActivity).setView(ivBinding.root).show()
|
||||
}
|
||||
}
|
||||
}.show()
|
||||
}
|
||||
1 -> {
|
||||
if (AngConfigManager.share2Clipboard(position) == 0) {
|
||||
if (AngConfigManager.share2Clipboard(mActivity, guid) == 0) {
|
||||
mActivity.toast(R.string.toast_success)
|
||||
} else {
|
||||
mActivity.toast(R.string.toast_failure)
|
||||
}
|
||||
}
|
||||
2 -> {
|
||||
if (AngConfigManager.shareFullContent2Clipboard(position) == 0) {
|
||||
mActivity.toast(R.string.toast_success)
|
||||
} else {
|
||||
mActivity.toast(R.string.toast_failure)
|
||||
}
|
||||
}
|
||||
else ->
|
||||
mActivity.toast("else")
|
||||
2 -> shareFullContent(guid)
|
||||
else -> mActivity.toast("else")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
}.show()
|
||||
}
|
||||
|
||||
holder.layout_edit.setOnClickListener {
|
||||
if (configType == AppConfig.EConfigType.Vmess) {
|
||||
mActivity.startActivity<ServerActivity>("position" to position, "isRunning" to !changeable)
|
||||
} else if (configType == AppConfig.EConfigType.Custom) {
|
||||
mActivity.startActivity<Server2Activity>("position" to position, "isRunning" to !changeable)
|
||||
} else if (configType == AppConfig.EConfigType.Shadowsocks) {
|
||||
mActivity.startActivity<Server3Activity>("position" to position, "isRunning" to !changeable)
|
||||
} else if (configType == AppConfig.EConfigType.Socks) {
|
||||
mActivity.startActivity<Server4Activity>("position" to position, "isRunning" to !changeable)
|
||||
}
|
||||
}
|
||||
holder.layout_remove.setOnClickListener {
|
||||
if (configs.index != position) {
|
||||
if (AngConfigManager.removeServer(position) == 0) {
|
||||
notifyItemRemoved(position)
|
||||
updateSelectedItem(position)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
holder.infoContainer.setOnClickListener {
|
||||
if (changeable) {
|
||||
AngConfigManager.setActiveServer(position)
|
||||
holder.itemMainBinding.layoutEdit.setOnClickListener {
|
||||
val intent = Intent().putExtra("guid", guid)
|
||||
.putExtra("isRunning", isRunning)
|
||||
if (config.configType == EConfigType.CUSTOM) {
|
||||
mActivity.startActivity(intent.setClass(mActivity, ServerCustomConfigActivity::class.java))
|
||||
} else {
|
||||
mActivity.startActivity(intent.setClass(mActivity, ServerActivity::class.java))
|
||||
}
|
||||
}
|
||||
holder.itemMainBinding.layoutRemove.setOnClickListener {
|
||||
if (guid != mainStorage?.decodeString(MmkvManager.KEY_SELECTED_SERVER)) {
|
||||
if (settingsStorage?.decodeBool(AppConfig.PREF_CONFIRM_REMOVE) == true) {
|
||||
AlertDialog.Builder(mActivity).setMessage(R.string.del_config_comfirm)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
removeServer(guid, position)
|
||||
}
|
||||
.show()
|
||||
} else {
|
||||
removeServer(guid, position)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
holder.itemMainBinding.infoContainer.setOnClickListener {
|
||||
val selected = mainStorage?.decodeString(MmkvManager.KEY_SELECTED_SERVER)
|
||||
if (guid != selected) {
|
||||
mainStorage?.encode(MmkvManager.KEY_SELECTED_SERVER, guid)
|
||||
if (!TextUtils.isEmpty(selected)) {
|
||||
notifyItemChanged(mActivity.mainViewModel.getPosition(selected!!))
|
||||
}
|
||||
notifyItemChanged(mActivity.mainViewModel.getPosition(guid))
|
||||
if (isRunning) {
|
||||
mActivity.showCircle()
|
||||
Utils.stopVService(mActivity)
|
||||
AngConfigManager.setActiveServer(position)
|
||||
Observable.timer(500, TimeUnit.MILLISECONDS)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe {
|
||||
mActivity.showCircle()
|
||||
if (!Utils.startVService(mActivity)) {
|
||||
V2RayServiceManager.startV2Ray(mActivity)
|
||||
mActivity.hideCircle()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
if (holder is FooterViewHolder) {
|
||||
//if (activity?.defaultDPreference?.getPrefBoolean(AppConfig.PREF_INAPP_BUY_IS_PREMIUM, false)) {
|
||||
if (true) {
|
||||
holder.layout_edit.visibility = View.INVISIBLE
|
||||
holder.itemFooterBinding.layoutEdit.visibility = View.INVISIBLE
|
||||
} else {
|
||||
holder.layout_edit.setOnClickListener {
|
||||
Utils.openUri(mActivity, AppConfig.promotionUrl)
|
||||
holder.itemFooterBinding.layoutEdit.setOnClickListener {
|
||||
Utils.openUri(mActivity, "${Utils.decode(AppConfig.promotionUrl)}?t=${System.currentTimeMillis()}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun shareFullContent(guid: String) {
|
||||
if (AngConfigManager.shareFullContent2Clipboard(mActivity, guid) == 0) {
|
||||
mActivity.toast(R.string.toast_success)
|
||||
} else {
|
||||
mActivity.toast(R.string.toast_failure)
|
||||
}
|
||||
}
|
||||
|
||||
private fun removeServer(guid: String,position:Int) {
|
||||
mActivity.mainViewModel.removeServer(guid)
|
||||
notifyItemRemoved(position)
|
||||
notifyItemRangeChanged(position, mActivity.mainViewModel.serversCache.size)
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseViewHolder {
|
||||
when (viewType) {
|
||||
return when (viewType) {
|
||||
VIEW_TYPE_ITEM ->
|
||||
return MainViewHolder(parent.context.layoutInflater
|
||||
.inflate(R.layout.item_recycler_main, parent, false))
|
||||
MainViewHolder(ItemRecyclerMainBinding.inflate(LayoutInflater.from(parent.context), parent, false))
|
||||
else ->
|
||||
return FooterViewHolder(parent.context.layoutInflater
|
||||
.inflate(R.layout.item_recycler_footer, parent, false))
|
||||
FooterViewHolder(ItemRecyclerFooterBinding.inflate(LayoutInflater.from(parent.context), parent, false))
|
||||
}
|
||||
}
|
||||
|
||||
fun updateConfigList() {
|
||||
configs = AngConfigManager.configs
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
// fun updateSelectedItem() {
|
||||
// updateSelectedItem(configs.index)
|
||||
// }
|
||||
|
||||
fun updateSelectedItem(pos: Int) {
|
||||
//notifyItemChanged(pos)
|
||||
notifyItemRangeChanged(pos, itemCount - pos)
|
||||
}
|
||||
|
||||
override fun getItemViewType(position: Int): Int {
|
||||
if (position == configs.vmess.count()) {
|
||||
return VIEW_TYPE_FOOTER
|
||||
return if (position == mActivity.mainViewModel.serversCache.size) {
|
||||
VIEW_TYPE_FOOTER
|
||||
} else {
|
||||
return VIEW_TYPE_ITEM
|
||||
VIEW_TYPE_ITEM
|
||||
}
|
||||
}
|
||||
|
||||
open class BaseViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView)
|
||||
|
||||
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() {
|
||||
open class BaseViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||
fun onItemSelected() {
|
||||
itemView.setBackgroundColor(Color.LTGRAY)
|
||||
}
|
||||
|
||||
override fun onItemClear() {
|
||||
fun onItemClear() {
|
||||
itemView.setBackgroundColor(0)
|
||||
}
|
||||
}
|
||||
|
||||
class FooterViewHolder(itemView: View) : BaseViewHolder(itemView), ItemTouchHelperViewHolder {
|
||||
val layout_edit = itemView.layout_edit!!
|
||||
class MainViewHolder(val itemMainBinding: ItemRecyclerMainBinding) :
|
||||
BaseViewHolder(itemMainBinding.root), ItemTouchHelperViewHolder
|
||||
|
||||
override fun onItemSelected() {
|
||||
itemView.setBackgroundColor(Color.LTGRAY)
|
||||
}
|
||||
|
||||
override fun onItemClear() {
|
||||
itemView.setBackgroundColor(0)
|
||||
}
|
||||
}
|
||||
class FooterViewHolder(val itemFooterBinding: ItemRecyclerFooterBinding) :
|
||||
BaseViewHolder(itemFooterBinding.root), ItemTouchHelperViewHolder
|
||||
|
||||
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) {
|
||||
// positiveButton(android.R.string.ok) {
|
||||
if (AngConfigManager.removeServer(position) == 0) {
|
||||
mActivity.mainViewModel.removeServer(guid)
|
||||
notifyItemRemoved(position)
|
||||
}
|
||||
// }
|
||||
// show()
|
||||
// }
|
||||
}
|
||||
updateSelectedItem(position)
|
||||
}
|
||||
|
||||
override fun onItemMove(fromPosition: Int, toPosition: Int): Boolean {
|
||||
AngConfigManager.swapServer(fromPosition, toPosition)
|
||||
mActivity.mainViewModel.swapServer(fromPosition, toPosition)
|
||||
notifyItemMoved(fromPosition, toPosition)
|
||||
//notifyDataSetChanged()
|
||||
updateSelectedItem(if (fromPosition < toPosition) fromPosition else toPosition)
|
||||
// position is changed, since position is used by click callbacks, need to update range
|
||||
if (toPosition > fromPosition)
|
||||
notifyItemRangeChanged(fromPosition, toPosition - fromPosition + 1)
|
||||
else
|
||||
notifyItemRangeChanged(toPosition, fromPosition - toPosition + 1)
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onItemMoveCompleted() {
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,57 +1,52 @@
|
||||
package com.v2ray.ang.ui
|
||||
|
||||
import android.animation.Animator
|
||||
import android.animation.AnimatorListenerAdapter
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.support.v7.widget.RecyclerView
|
||||
import android.text.TextUtils
|
||||
import android.util.Log
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.animation.AccelerateInterpolator
|
||||
import android.view.animation.DecelerateInterpolator
|
||||
import com.dinuscxj.itemdecoration.LinearDividerItemDecoration
|
||||
import androidx.appcompat.widget.SearchView
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.preference.PreferenceManager
|
||||
import androidx.recyclerview.widget.DividerItemDecoration
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.v2ray.ang.AppConfig
|
||||
import com.v2ray.ang.AppConfig.ANG_PACKAGE
|
||||
import com.v2ray.ang.R
|
||||
import com.v2ray.ang.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 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.schedulers.Schedulers
|
||||
import java.text.Collator
|
||||
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() {
|
||||
companion object {
|
||||
const val PREF_PER_APP_PROXY_SET = "pref_per_app_proxy_set"
|
||||
const val PREF_BYPASS_APPS = "pref_bypass_apps"
|
||||
}
|
||||
private lateinit var binding: ActivityBypassListBinding
|
||||
|
||||
private var adapter: PerAppProxyAdapter? = null
|
||||
private var appsAll: List<AppInfo>? = null
|
||||
private val defaultSharedPreferences by lazy { PreferenceManager.getDefaultSharedPreferences(this) }
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_bypass_list)
|
||||
binding = ActivityBypassListBinding.inflate(layoutInflater)
|
||||
val view = binding.root
|
||||
setContentView(view)
|
||||
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
|
||||
val dividerItemDecoration = LinearDividerItemDecoration(
|
||||
this, LinearDividerItemDecoration.LINEAR_DIVIDER_VERTICAL)
|
||||
recycler_view.addItemDecoration(dividerItemDecoration)
|
||||
val dividerItemDecoration = DividerItemDecoration(this, LinearLayoutManager.VERTICAL)
|
||||
binding.recyclerView.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)
|
||||
.subscribeOn(Schedulers.io())
|
||||
@@ -64,8 +59,8 @@ class PerAppProxyActivity : BaseActivity() {
|
||||
one.isSelected = 0
|
||||
}
|
||||
}
|
||||
val comparator = object : Comparator<AppInfo> {
|
||||
override fun compare(p1: AppInfo, p2: AppInfo): Int = when {
|
||||
val comparator = Comparator<AppInfo> { p1, p2 ->
|
||||
when {
|
||||
p1.isSelected > p2.isSelected -> -1
|
||||
p1.isSelected == p2.isSelected -> 0
|
||||
else -> 1
|
||||
@@ -91,13 +86,13 @@ class PerAppProxyActivity : BaseActivity() {
|
||||
.subscribe {
|
||||
appsAll = it
|
||||
adapter = PerAppProxyAdapter(this, it, blacklist)
|
||||
recycler_view.adapter = adapter
|
||||
pb_waiting.visibility = View.GONE
|
||||
binding.recyclerView.adapter = adapter
|
||||
binding.pbWaiting.visibility = View.GONE
|
||||
}
|
||||
|
||||
/***
|
||||
recycler_view.addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
||||
var dst = 0
|
||||
val threshold = resources.getDimensionPixelSize(R.dimen.bypass_list_header_height) * 3
|
||||
val threshold = resources.getDimensionPixelSize(R.dimen.bypass_list_header_height) * 2
|
||||
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
||||
dst += dy
|
||||
if (dst > threshold) {
|
||||
@@ -139,17 +134,19 @@ class PerAppProxyActivity : BaseActivity() {
|
||||
showing = true
|
||||
}
|
||||
})
|
||||
***/
|
||||
|
||||
switch_per_app_proxy.setOnCheckedChangeListener { buttonView, isChecked ->
|
||||
defaultDPreference.setPrefBoolean(SettingsActivity.PREF_PER_APP_PROXY, isChecked)
|
||||
binding.switchPerAppProxy.setOnCheckedChangeListener { _, isChecked ->
|
||||
defaultSharedPreferences.edit().putBoolean(AppConfig.PREF_PER_APP_PROXY, isChecked).apply()
|
||||
}
|
||||
switch_per_app_proxy.isChecked = defaultDPreference.getPrefBoolean(SettingsActivity.PREF_PER_APP_PROXY, false)
|
||||
binding.switchPerAppProxy.isChecked = defaultSharedPreferences.getBoolean(AppConfig.PREF_PER_APP_PROXY, false)
|
||||
|
||||
switch_bypass_apps.setOnCheckedChangeListener { buttonView, isChecked ->
|
||||
defaultDPreference.setPrefBoolean(PREF_BYPASS_APPS, isChecked)
|
||||
binding.switchBypassApps.setOnCheckedChangeListener { _, isChecked ->
|
||||
defaultSharedPreferences.edit().putBoolean(AppConfig.PREF_BYPASS_APPS, isChecked).apply()
|
||||
}
|
||||
switch_bypass_apps.isChecked = defaultDPreference.getPrefBoolean(PREF_BYPASS_APPS, false)
|
||||
binding.switchBypassApps.isChecked = defaultSharedPreferences.getBoolean(AppConfig.PREF_BYPASS_APPS, false)
|
||||
|
||||
/***
|
||||
et_search.setOnEditorActionListener { v, actionId, event ->
|
||||
if (actionId == EditorInfo.IME_ACTION_SEARCH) {
|
||||
//hide
|
||||
@@ -177,17 +174,35 @@ class PerAppProxyActivity : BaseActivity() {
|
||||
false
|
||||
}
|
||||
}
|
||||
***/
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
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)
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -204,14 +219,20 @@ class PerAppProxyActivity : BaseActivity() {
|
||||
val packageName = it.packageName
|
||||
adapter?.blacklist!!.add(packageName)
|
||||
}
|
||||
|
||||
}
|
||||
it.notifyDataSetChanged()
|
||||
true
|
||||
} ?: false
|
||||
R.id.select_proxy_app -> {
|
||||
selectProxyApp()
|
||||
|
||||
true
|
||||
}
|
||||
R.id.import_proxy_app -> {
|
||||
importProxyApp()
|
||||
true
|
||||
}
|
||||
R.id.export_proxy_app -> {
|
||||
exportProxyApp()
|
||||
true
|
||||
}
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
@@ -220,22 +241,41 @@ class PerAppProxyActivity : BaseActivity() {
|
||||
private fun selectProxyApp() {
|
||||
toast(R.string.msg_downloading_content)
|
||||
val url = AppConfig.androidpackagenamelistUrl
|
||||
doAsync {
|
||||
val content = URL(url).readText()
|
||||
uiThread {
|
||||
Log.d("selectProxyApp", content)
|
||||
selectProxyApp(content)
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
val content = Utils.getUrlContext(url, 5000)
|
||||
launch(Dispatchers.Main) {
|
||||
Log.d(ANG_PACKAGE, content)
|
||||
selectProxyApp(content, true)
|
||||
toast(R.string.toast_success)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun selectProxyApp(content: String): Boolean {
|
||||
try {
|
||||
var proxyApps = content
|
||||
private fun importProxyApp() {
|
||||
val content = Utils.getClipboard(applicationContext)
|
||||
if (TextUtils.isEmpty(content)) {
|
||||
val assets = Utils.readTextFromAssets(v2RayApplication, "proxy_packagename.txt")
|
||||
proxyApps = assets.lines().toString()
|
||||
return
|
||||
}
|
||||
selectProxyApp(content, false)
|
||||
toast(R.string.toast_success)
|
||||
}
|
||||
|
||||
private fun exportProxyApp() {
|
||||
var lst = binding.switchBypassApps.isChecked.toString()
|
||||
|
||||
adapter?.blacklist?.forEach block@{
|
||||
lst = lst + System.getProperty("line.separator") + it
|
||||
}
|
||||
Utils.setClipboard(applicationContext, lst)
|
||||
toast(R.string.toast_success)
|
||||
}
|
||||
|
||||
private fun selectProxyApp(content: String, force: Boolean): Boolean {
|
||||
try {
|
||||
val proxyApps = if (TextUtils.isEmpty(content)) {
|
||||
Utils.readTextFromAssets(v2RayApplication, "proxy_packagename.txt")
|
||||
} else {
|
||||
content
|
||||
}
|
||||
if (TextUtils.isEmpty(proxyApps)) {
|
||||
return false
|
||||
@@ -243,12 +283,12 @@ class PerAppProxyActivity : BaseActivity() {
|
||||
|
||||
adapter?.blacklist!!.clear()
|
||||
|
||||
if (switch_bypass_apps.isChecked) {
|
||||
if (binding.switchBypassApps.isChecked) {
|
||||
adapter?.let {
|
||||
it.apps.forEach block@{
|
||||
val packageName = it.packageName
|
||||
Log.d("selectProxyApp2", packageName)
|
||||
if (proxyApps.indexOf(packageName) < 0) {
|
||||
Log.d(ANG_PACKAGE, packageName)
|
||||
if (!inProxyApps(proxyApps, packageName, force)) {
|
||||
adapter?.blacklist!!.add(packageName)
|
||||
println(packageName)
|
||||
return@block
|
||||
@@ -260,8 +300,8 @@ class PerAppProxyActivity : BaseActivity() {
|
||||
adapter?.let {
|
||||
it.apps.forEach block@{
|
||||
val packageName = it.packageName
|
||||
Log.d("selectProxyApp3", packageName)
|
||||
if (proxyApps.indexOf(packageName) >= 0) {
|
||||
Log.d(ANG_PACKAGE, packageName)
|
||||
if (inProxyApps(proxyApps, packageName, force)) {
|
||||
adapter?.blacklist!!.add(packageName)
|
||||
println(packageName)
|
||||
return@block
|
||||
@@ -276,4 +316,40 @@ class PerAppProxyActivity : BaseActivity() {
|
||||
}
|
||||
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
|
||||
|
||||
import android.graphics.Color
|
||||
import android.support.v7.widget.RecyclerView
|
||||
import android.view.LayoutInflater
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import com.v2ray.ang.R
|
||||
import com.v2ray.ang.databinding.ItemRecyclerBypassListBinding
|
||||
import com.v2ray.ang.dto.AppInfo
|
||||
import 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.*
|
||||
|
||||
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 var mActivity: BaseActivity = activity
|
||||
val blacklist = if (blacklist == null) HashSet<String>() else HashSet<String>(blacklist)
|
||||
val blacklist = if (blacklist == null) HashSet() else HashSet(blacklist)
|
||||
|
||||
override fun onBindViewHolder(holder: BaseViewHolder, position: Int) {
|
||||
if (holder is AppViewHolder) {
|
||||
@@ -39,14 +35,13 @@ class PerAppProxyAdapter(val activity: BaseActivity, val apps: List<AppInfo>, bl
|
||||
VIEW_TYPE_HEADER -> {
|
||||
val view = View(ctx)
|
||||
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)
|
||||
}
|
||||
// VIEW_TYPE_ITEM -> AppViewHolder(ctx.layoutInflater
|
||||
// .inflate(R.layout.item_recycler_bypass_list, parent, false))
|
||||
|
||||
else -> AppViewHolder(ctx.layoutInflater
|
||||
.inflate(R.layout.item_recycler_bypass_list, parent, false))
|
||||
else -> AppViewHolder(ItemRecyclerBypassListBinding.inflate(LayoutInflater.from(ctx), parent, false))
|
||||
|
||||
}
|
||||
}
|
||||
@@ -55,30 +50,25 @@ class PerAppProxyAdapter(val activity: BaseActivity, val apps: List<AppInfo>, bl
|
||||
|
||||
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 {
|
||||
private val inBlacklist: Boolean get() = blacklist.contains(appInfo.packageName)
|
||||
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) {
|
||||
this.appInfo = appInfo
|
||||
|
||||
icon.image = appInfo.appIcon
|
||||
itemBypassBinding.icon.setImageDrawable(appInfo.appIcon)
|
||||
// name.text = appInfo.appName
|
||||
|
||||
checkBox.isChecked = inBlacklist
|
||||
package_name.text = appInfo.packageName
|
||||
itemBypassBinding.checkBox.isChecked = inBlacklist
|
||||
itemBypassBinding.packageName.text = appInfo.packageName
|
||||
if (appInfo.isSystemApp) {
|
||||
name.text = String.format("** %1s", appInfo.appName)
|
||||
name.textColor = Color.RED
|
||||
itemBypassBinding.name.text = String.format("** %1s", appInfo.appName)
|
||||
//name.textColor = Color.RED
|
||||
} else {
|
||||
name.text = appInfo.appName
|
||||
name.textColor = Color.DKGRAY
|
||||
itemBypassBinding.name.text = appInfo.appName
|
||||
//name.textColor = Color.DKGRAY
|
||||
}
|
||||
|
||||
itemView.setOnClickListener(this)
|
||||
@@ -87,10 +77,10 @@ class PerAppProxyAdapter(val activity: BaseActivity, val apps: List<AppInfo>, bl
|
||||
override fun onClick(v: View?) {
|
||||
if (inBlacklist) {
|
||||
blacklist.remove(appInfo.packageName)
|
||||
checkBox.isChecked = false
|
||||
itemBypassBinding.checkBox.isChecked = false
|
||||
} else {
|
||||
blacklist.add(appInfo.packageName)
|
||||
checkBox.isChecked = true
|
||||
itemBypassBinding.checkBox.isChecked = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,23 +1,26 @@
|
||||
package com.v2ray.ang.ui
|
||||
|
||||
import android.graphics.Color
|
||||
import android.os.Bundle
|
||||
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 kotlinx.android.synthetic.main.activity_routing_settings.*
|
||||
|
||||
import com.v2ray.ang.databinding.ActivityRoutingSettingsBinding
|
||||
|
||||
class RoutingSettingsActivity : BaseActivity() {
|
||||
private lateinit var binding: ActivityRoutingSettingsBinding
|
||||
|
||||
private val titles: Array<out String> by lazy {
|
||||
resources.getStringArray(R.array.routing_tag)
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
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)
|
||||
|
||||
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_BLOCKED))
|
||||
|
||||
val adapter = FragmentAdapter(supportFragmentManager, fragments, titles.toList())
|
||||
viewpager?.adapter = adapter
|
||||
tablayout.setTabTextColors(Color.BLACK, Color.RED)
|
||||
tablayout.setupWithViewPager(viewpager)
|
||||
val adapter = FragmentAdapter(this, fragments)
|
||||
binding.viewpager.adapter = adapter
|
||||
//tablayout.setTabTextColors(Color.BLACK, Color.RED)
|
||||
TabLayoutMediator(binding.tablayout, binding.viewpager) { tab, position ->
|
||||
tab.text = titles[position]
|
||||
}.attach()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,41 +1,39 @@
|
||||
package com.v2ray.ang.ui
|
||||
|
||||
|
||||
import android.Manifest
|
||||
import android.app.Activity.RESULT_OK
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.support.v4.app.Fragment
|
||||
import android.text.TextUtils
|
||||
import android.util.Log
|
||||
import android.view.*
|
||||
import com.v2ray.ang.R
|
||||
import com.v2ray.ang.extension.defaultDPreference
|
||||
import com.v2ray.ang.util.Utils
|
||||
import kotlinx.android.synthetic.main.fragment_routing_settings.*
|
||||
import org.jetbrains.anko.toast
|
||||
import android.view.MenuInflater
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.tbruyelle.rxpermissions.RxPermissions
|
||||
import com.v2ray.ang.AppConfig
|
||||
import org.jetbrains.anko.doAsync
|
||||
import org.jetbrains.anko.startActivityForResult
|
||||
import org.jetbrains.anko.support.v4.startActivityForResult
|
||||
import org.jetbrains.anko.support.v4.toast
|
||||
import org.jetbrains.anko.uiThread
|
||||
import java.net.URL
|
||||
|
||||
import com.v2ray.ang.R
|
||||
import com.v2ray.ang.databinding.FragmentRoutingSettingsBinding
|
||||
import com.v2ray.ang.extension.toast
|
||||
import com.v2ray.ang.extension.v2RayApplication
|
||||
import com.v2ray.ang.util.Utils
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class RoutingSettingsFragment : Fragment() {
|
||||
private lateinit var binding: FragmentRoutingSettingsBinding
|
||||
companion object {
|
||||
private const val routing_arg = "routing_arg"
|
||||
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?,
|
||||
savedInstanceState: Bundle?): View? {
|
||||
// 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 {
|
||||
@@ -46,11 +44,11 @@ class RoutingSettingsFragment : Fragment() {
|
||||
return fragment
|
||||
}
|
||||
|
||||
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||
super.onActivityCreated(savedInstanceState)
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
val content = activity?.defaultDPreference?.getPrefString(arguments!!.getString(routing_arg), "")
|
||||
et_routing_content.text = Utils.getEditable(content!!)
|
||||
val content = defaultSharedPreferences.getString(requireArguments().getString(routing_arg), "")
|
||||
binding.etRoutingContent.text = Utils.getEditable(content!!)
|
||||
|
||||
setHasOptionsMenu(true)
|
||||
}
|
||||
@@ -62,21 +60,19 @@ class RoutingSettingsFragment : Fragment() {
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
|
||||
R.id.save_routing -> {
|
||||
val content = et_routing_content.text.toString()
|
||||
activity?.defaultDPreference?.setPrefString(arguments!!.getString(routing_arg), content)
|
||||
activity?.toast(R.string.toast_success)
|
||||
saveRouting()
|
||||
true
|
||||
}
|
||||
R.id.del_routing -> {
|
||||
et_routing_content.text = null
|
||||
binding.etRoutingContent.text = null
|
||||
true
|
||||
}
|
||||
R.id.scan_replace -> {
|
||||
scanQRcode(REQUEST_SCAN_REPLACE)
|
||||
scanQRcode(true)
|
||||
true
|
||||
}
|
||||
R.id.scan_append -> {
|
||||
scanQRcode(REQUEST_SCAN_APPEND)
|
||||
scanQRcode(false)
|
||||
true
|
||||
}
|
||||
R.id.default_rules -> {
|
||||
@@ -86,17 +82,26 @@ class RoutingSettingsFragment : Fragment() {
|
||||
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 {
|
||||
// startActivityForResult(Intent("com.google.zxing.client.android.SCAN")
|
||||
// .addCategory(Intent.CATEGORY_DEFAULT)
|
||||
// .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP), requestCode)
|
||||
// } catch (e: Exception) {
|
||||
RxPermissions(activity!!)
|
||||
RxPermissions(requireActivity())
|
||||
.request(Manifest.permission.CAMERA)
|
||||
.subscribe {
|
||||
if (it)
|
||||
startActivityForResult<ScannerActivity>(requestCode)
|
||||
if (forReplace)
|
||||
scanQRCodeForReplace.launch(Intent(activity, ScannerActivity::class.java))
|
||||
else
|
||||
scanQRCodeForAppend.launch(Intent(activity, ScannerActivity::class.java))
|
||||
else
|
||||
activity?.toast(R.string.toast_permission_denied)
|
||||
}
|
||||
@@ -104,46 +109,50 @@ class RoutingSettingsFragment : Fragment() {
|
||||
return true
|
||||
}
|
||||
|
||||
fun setDefaultRules(): Boolean {
|
||||
var url = AppConfig.v2rayCustomRoutingListUrl
|
||||
when (arguments!!.getString(routing_arg)) {
|
||||
AppConfig.PREF_V2RAY_ROUTING_AGENT -> {
|
||||
url += AppConfig.TAG_AGENT
|
||||
}
|
||||
AppConfig.PREF_V2RAY_ROUTING_DIRECT -> {
|
||||
url += AppConfig.TAG_DIRECT
|
||||
}
|
||||
AppConfig.PREF_V2RAY_ROUTING_BLOCKED -> {
|
||||
url += AppConfig.TAG_BLOCKED
|
||||
private val scanQRCodeForReplace = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||
if (it.resultCode == RESULT_OK) {
|
||||
val content = it.data?.getStringExtra("SCAN_RESULT")
|
||||
binding.etRoutingContent.text = Utils.getEditable(content!!)
|
||||
}
|
||||
}
|
||||
|
||||
toast(R.string.msg_downloading_content)
|
||||
doAsync {
|
||||
val content = URL(url).readText()
|
||||
uiThread {
|
||||
et_routing_content.text = Utils.getEditable(content!!)
|
||||
toast(R.string.toast_success)
|
||||
private val scanQRCodeForAppend = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||
if (it.resultCode == RESULT_OK) {
|
||||
val content = it.data?.getStringExtra("SCAN_RESULT")
|
||||
binding.etRoutingContent.text = Utils.getEditable("${binding.etRoutingContent.text},$content")
|
||||
}
|
||||
}
|
||||
|
||||
fun setDefaultRules(): Boolean {
|
||||
var url = AppConfig.v2rayCustomRoutingListUrl
|
||||
var tag = ""
|
||||
when (requireArguments().getString(routing_arg)) {
|
||||
AppConfig.PREF_V2RAY_ROUTING_AGENT -> {
|
||||
tag = AppConfig.TAG_AGENT
|
||||
}
|
||||
AppConfig.PREF_V2RAY_ROUTING_DIRECT -> {
|
||||
tag = AppConfig.TAG_DIRECT
|
||||
}
|
||||
AppConfig.PREF_V2RAY_ROUTING_BLOCKED -> {
|
||||
tag = AppConfig.TAG_BLOCKED
|
||||
}
|
||||
}
|
||||
url += tag
|
||||
|
||||
activity?.toast(R.string.msg_downloading_content)
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
val content = Utils.getUrlContext(url, 5000)
|
||||
launch(Dispatchers.Main) {
|
||||
val routingList = if (TextUtils.isEmpty(content)) {
|
||||
Utils.readTextFromAssets(activity?.v2RayApplication!!, "custom_routing_$tag")
|
||||
} else {
|
||||
content
|
||||
}
|
||||
binding.etRoutingContent.text = Utils.getEditable(routingList)
|
||||
saveRouting()
|
||||
//toast(R.string.toast_success)
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
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.util.AngConfigManager
|
||||
import android.os.Bundle
|
||||
import org.jetbrains.anko.*
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import com.v2ray.ang.extension.toast
|
||||
|
||||
class ScScannerActivity : BaseActivity() {
|
||||
companion object {
|
||||
private const val REQUEST_SCAN = 1
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_none)
|
||||
importQRcode(REQUEST_SCAN)
|
||||
importQRcode()
|
||||
}
|
||||
|
||||
fun importQRcode(requestCode: Int): Boolean {
|
||||
fun importQRcode(): Boolean {
|
||||
RxPermissions(this)
|
||||
.request(Manifest.permission.CAMERA)
|
||||
.subscribe {
|
||||
if (it)
|
||||
startActivityForResult<ScannerActivity>(requestCode)
|
||||
scanQRCode.launch(Intent(this, ScannerActivity::class.java))
|
||||
else
|
||||
toast(R.string.toast_permission_denied)
|
||||
}
|
||||
@@ -32,21 +30,16 @@ class ScScannerActivity : BaseActivity() {
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
when (requestCode) {
|
||||
REQUEST_SCAN ->
|
||||
if (resultCode == RESULT_OK) {
|
||||
val count = AngConfigManager.importBatchConfig(data?.getStringExtra("SCAN_RESULT"), "")
|
||||
private val scanQRCode = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||
if (it.resultCode == RESULT_OK) {
|
||||
val count = AngConfigManager.importBatchConfig(it.data?.getStringExtra("SCAN_RESULT"), "", false)
|
||||
if (count > 0) {
|
||||
toast(R.string.toast_success)
|
||||
} else {
|
||||
toast(R.string.toast_failure)
|
||||
}
|
||||
startActivity<MainActivity>()
|
||||
}
|
||||
startActivity(Intent(this, MainActivity::class.java))
|
||||
}
|
||||
finish()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,104 +1,22 @@
|
||||
package com.v2ray.ang.ui
|
||||
|
||||
import android.content.*
|
||||
import android.net.VpnService
|
||||
import com.v2ray.ang.R
|
||||
import com.v2ray.ang.util.Utils
|
||||
import android.os.Bundle
|
||||
import com.v2ray.ang.AppConfig
|
||||
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
|
||||
import com.v2ray.ang.service.V2RayServiceManager
|
||||
|
||||
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?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
moveTaskToBack(true)
|
||||
|
||||
setContentView(R.layout.activity_none)
|
||||
|
||||
val isRunning = Utils.isServiceRun(this, "com.v2ray.ang.service.V2RayVpnService")
|
||||
if (isRunning) {
|
||||
//Utils.stopVService(this)
|
||||
mMsgReceive = ReceiveMessageHandler(this@ScSwitchActivity)
|
||||
registerReceiver(mMsgReceive, IntentFilter(AppConfig.BROADCAST_ACTION_ACTIVITY))
|
||||
MessageUtil.sendMsg2Service(this, AppConfig.MSG_REGISTER_CLIENT, "")
|
||||
|
||||
if (V2RayServiceManager.v2rayPoint.isRunning) {
|
||||
Utils.stopVService(this)
|
||||
} else {
|
||||
Utils.startVService(this)
|
||||
finishActivity()
|
||||
Utils.startVServiceFromToggle(this)
|
||||
}
|
||||
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 android.content.Intent
|
||||
import android.graphics.BitmapFactory
|
||||
import android.icu.util.TimeUnit
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import com.google.zxing.BarcodeFormat
|
||||
import com.tbruyelle.rxpermissions.RxPermissions
|
||||
import com.v2ray.ang.R
|
||||
import com.v2ray.ang.extension.toast
|
||||
import com.v2ray.ang.util.QRCodeDecoder
|
||||
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 {
|
||||
companion object {
|
||||
private const val REQUEST_FILE_CHOOSER = 2
|
||||
}
|
||||
|
||||
|
||||
private var mScannerView: ZXingScannerView? = null
|
||||
|
||||
public override fun onCreate(state: Bundle?) {
|
||||
super.onCreate(state)
|
||||
public override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
mScannerView = ZXingScannerView(this) // Programmatically initialize the scanner view
|
||||
|
||||
mScannerView?.setAutoFocus(true)
|
||||
@@ -69,14 +56,14 @@ class ScannerActivity : BaseActivity(), ZXingScannerView.ResultHandler {
|
||||
// mScannerView!!.resumeCameraPreview(this)
|
||||
}
|
||||
|
||||
fun finished(text: String) {
|
||||
private fun finished(text: String) {
|
||||
val intent = Intent()
|
||||
intent.putExtra("SCAN_RESULT", text)
|
||||
setResult(Activity.RESULT_OK, intent)
|
||||
finish()
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
|
||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||
menuInflater.inflate(R.menu.menu_scanner, menu)
|
||||
return true
|
||||
}
|
||||
@@ -107,30 +94,23 @@ class ScannerActivity : BaseActivity(), ZXingScannerView.ResultHandler {
|
||||
//intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)
|
||||
|
||||
try {
|
||||
startActivityForResult(
|
||||
Intent.createChooser(intent, getString(R.string.title_file_chooser)),
|
||||
REQUEST_FILE_CHOOSER)
|
||||
chooseFile.launch(Intent.createChooser(intent, getString(R.string.title_file_chooser)))
|
||||
} catch (ex: android.content.ActivityNotFoundException) {
|
||||
toast(R.string.toast_require_file_manager)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
when (requestCode) {
|
||||
REQUEST_FILE_CHOOSER ->
|
||||
if (resultCode == RESULT_OK) {
|
||||
private val chooseFile = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||
val uri = it.data?.data
|
||||
if (it.resultCode == RESULT_OK && uri != null) {
|
||||
try {
|
||||
val uri = data!!.data
|
||||
val bitmap = BitmapFactory.decodeStream(contentResolver.openInputStream(uri))
|
||||
val text = QRCodeDecoder.syncDecodeQRCode(bitmap)
|
||||
finished(text)
|
||||
finished(text!!)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
toast(e.message.toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.widget.*
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import com.tencent.mmkv.MMKV
|
||||
import com.v2ray.ang.AppConfig
|
||||
import com.v2ray.ang.AppConfig.PREF_ALLOW_INSECURE
|
||||
import com.v2ray.ang.R
|
||||
import com.v2ray.ang.dto.AngConfig
|
||||
import com.v2ray.ang.util.AngConfigManager
|
||||
import com.v2ray.ang.dto.EConfigType
|
||||
import com.v2ray.ang.dto.ServerConfig
|
||||
import com.v2ray.ang.dto.V2rayConfig
|
||||
import com.v2ray.ang.dto.V2rayConfig.Companion.DEFAULT_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 kotlinx.android.synthetic.main.activity_server.*
|
||||
import org.jetbrains.anko.*
|
||||
|
||||
|
||||
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 {
|
||||
resources.getStringArray(R.array.securitys)
|
||||
}
|
||||
private val shadowsocksSecuritys: Array<out String> by lazy {
|
||||
resources.getStringArray(R.array.ss_securitys)
|
||||
}
|
||||
private val flows: Array<out String> by lazy {
|
||||
resources.getStringArray(R.array.flows)
|
||||
}
|
||||
private val networks: Array<out String> by lazy {
|
||||
resources.getStringArray(R.array.networks)
|
||||
}
|
||||
private val headertypes: Array<out String> by lazy {
|
||||
resources.getStringArray(R.array.headertypes)
|
||||
private val tcpTypes: Array<out String> by lazy {
|
||||
resources.getStringArray(R.array.header_type_tcp)
|
||||
}
|
||||
private val streamsecuritys: Array<out String> by lazy {
|
||||
resources.getStringArray(R.array.streamsecuritys)
|
||||
private val kcpAndQuicTypes: Array<out String> by lazy {
|
||||
resources.getStringArray(R.array.header_type_kcp_and_quic)
|
||||
}
|
||||
private val grpcModes: Array<out String> by lazy {
|
||||
resources.getStringArray(R.array.mode_type_grpc)
|
||||
}
|
||||
private val streamSecuritys: Array<out String> by lazy {
|
||||
resources.getStringArray(R.array.streamsecurityxs)
|
||||
}
|
||||
private val allowinsecures: Array<out String> by lazy {
|
||||
resources.getStringArray(R.array.allowinsecures)
|
||||
}
|
||||
private val uTlsItems: Array<out String> by lazy {
|
||||
resources.getStringArray(R.array.streamsecurity_utls)
|
||||
}
|
||||
private val alpns: Array<out String> by lazy {
|
||||
resources.getStringArray(R.array.streamsecurity_alpn)
|
||||
}
|
||||
// Kotlin synthetics was used, but since it is removed in 1.8. We switch to old manual approach.
|
||||
// We don't use AndroidViewBinding because, it is better to share similar logics for different
|
||||
// protocols. Use findViewById manually ensures the xml are de-coupled with the activity logic.
|
||||
private val et_remarks: EditText by lazy { findViewById(R.id.et_remarks) }
|
||||
private val et_address: EditText by lazy { findViewById(R.id.et_address) }
|
||||
private val et_port: EditText by lazy { findViewById(R.id.et_port) }
|
||||
private val et_id: EditText by lazy { findViewById(R.id.et_id) }
|
||||
private val et_alterId: EditText? by lazy { findViewById(R.id.et_alterId) }
|
||||
private val et_security: EditText? by lazy { findViewById(R.id.et_security) }
|
||||
private val sp_flow: Spinner? by lazy { findViewById(R.id.sp_flow) }
|
||||
private val sp_security: Spinner? by lazy { findViewById(R.id.sp_security) }
|
||||
private val sp_stream_security: Spinner? by lazy { findViewById(R.id.sp_stream_security) }
|
||||
private val sp_allow_insecure: Spinner? by lazy { findViewById(R.id.sp_allow_insecure) }
|
||||
private val et_sni: EditText? by lazy { findViewById(R.id.et_sni) }
|
||||
private val sp_stream_fingerprint: Spinner? by lazy { findViewById(R.id.sp_stream_fingerprint) } //uTLS
|
||||
private val sp_network: Spinner? by lazy { findViewById(R.id.sp_network) }
|
||||
private val sp_header_type: Spinner? by lazy { findViewById(R.id.sp_header_type) }
|
||||
private val sp_header_type_title: TextView? by lazy { findViewById(R.id.sp_header_type_title) }
|
||||
private val et_request_host: EditText? by lazy { findViewById(R.id.et_request_host) }
|
||||
private val et_path: EditText? by lazy { findViewById(R.id.et_path) }
|
||||
private val sp_stream_alpn: Spinner? by lazy { findViewById(R.id.sp_stream_alpn) } //uTLS
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
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)
|
||||
|
||||
if (edit_index >= 0) {
|
||||
edit_guid = configs.vmess[edit_index].guid
|
||||
bindingServer(configs.vmess[edit_index])
|
||||
val config = MmkvManager.decodeServerConfig(editGuid)
|
||||
when(config?.configType ?: createConfigType) {
|
||||
EConfigType.VMESS -> setContentView(R.layout.activity_server_vmess)
|
||||
EConfigType.CUSTOM -> return
|
||||
EConfigType.SHADOWSOCKS -> setContentView(R.layout.activity_server_shadowsocks)
|
||||
EConfigType.SOCKS -> setContentView(R.layout.activity_server_socks)
|
||||
EConfigType.VLESS -> setContentView(R.layout.activity_server_vless)
|
||||
EConfigType.TROJAN -> setContentView(R.layout.activity_server_trojan)
|
||||
}
|
||||
sp_network?.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
|
||||
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
|
||||
val types = transportTypes(networks[position])
|
||||
sp_header_type?.isEnabled = types.size > 1
|
||||
val adapter = ArrayAdapter(this@ServerActivity, android.R.layout.simple_spinner_item, types)
|
||||
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
|
||||
sp_header_type?.adapter = adapter
|
||||
sp_header_type_title?.text = if (networks[position] == "grpc")
|
||||
getString(R.string.server_lab_mode_type) else
|
||||
getString(R.string.server_lab_head_type)
|
||||
config?.getProxyOutbound()?.getTransportSettingDetails()?.let { transportDetails ->
|
||||
sp_header_type?.setSelection(Utils.arrayFind(types, transportDetails[0]))
|
||||
et_request_host?.text = Utils.getEditable(transportDetails[1])
|
||||
et_path?.text = Utils.getEditable(transportDetails[2])
|
||||
}
|
||||
}
|
||||
override fun onNothingSelected(parent: AdapterView<*>?) {
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
if (config != null) {
|
||||
bindingServer(config)
|
||||
} else {
|
||||
clearServer()
|
||||
}
|
||||
@@ -58,33 +139,59 @@ class ServerActivity : BaseActivity() {
|
||||
/**
|
||||
* bingding seleced server config
|
||||
*/
|
||||
fun bindingServer(vmess: AngConfig.VmessBean): Boolean {
|
||||
et_remarks.text = Utils.getEditable(vmess.remarks)
|
||||
private fun bindingServer(config: ServerConfig): Boolean {
|
||||
val outbound = config.getProxyOutbound() ?: return false
|
||||
val streamSetting = config.outboundBean?.streamSettings ?: return false
|
||||
|
||||
et_address.text = Utils.getEditable(vmess.address)
|
||||
et_port.text = Utils.getEditable(vmess.port.toString())
|
||||
et_id.text = Utils.getEditable(vmess.id)
|
||||
et_alterId.text = Utils.getEditable(vmess.alterId.toString())
|
||||
|
||||
val security = Utils.arrayFind(securitys, vmess.security)
|
||||
et_remarks.text = Utils.getEditable(config.remarks)
|
||||
et_address.text = Utils.getEditable(outbound.getServerAddress().orEmpty())
|
||||
et_port.text = Utils.getEditable(outbound.getServerPort()?.toString() ?: DEFAULT_PORT.toString())
|
||||
et_id.text = Utils.getEditable(outbound.getPassword().orEmpty())
|
||||
et_alterId?.text = Utils.getEditable(outbound.settings?.vnext?.get(0)?.users?.get(0)?.alterId.toString())
|
||||
if (config.configType == EConfigType.SOCKS) {
|
||||
et_security?.text = Utils.getEditable(outbound.settings?.servers?.get(0)?.users?.get(0)?.user.orEmpty())
|
||||
} else if (config.configType == EConfigType.VLESS) {
|
||||
et_security?.text = Utils.getEditable(outbound.getSecurityEncryption().orEmpty())
|
||||
val flow = Utils.arrayFind(flows, outbound.settings?.vnext?.get(0)?.users?.get(0)?.flow.orEmpty())
|
||||
if (flow >= 0) {
|
||||
sp_flow?.setSelection(flow)
|
||||
}
|
||||
} else if (config.configType == EConfigType.TROJAN) {
|
||||
val flow = Utils.arrayFind(flows, outbound.settings?.servers?.get(0)?.flow.orEmpty())
|
||||
if (flow >= 0) {
|
||||
sp_flow?.setSelection(flow)
|
||||
}
|
||||
}
|
||||
val securityEncryptions = if (config.configType == EConfigType.SHADOWSOCKS) shadowsocksSecuritys else securitys
|
||||
val security = Utils.arrayFind(securityEncryptions, outbound.getSecurityEncryption().orEmpty())
|
||||
if (security >= 0) {
|
||||
sp_security.setSelection(security)
|
||||
}
|
||||
val network = Utils.arrayFind(networks, vmess.network)
|
||||
if (network >= 0) {
|
||||
sp_network.setSelection(network)
|
||||
sp_security?.setSelection(security)
|
||||
}
|
||||
|
||||
val headerType = Utils.arrayFind(headertypes, vmess.headerType)
|
||||
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)
|
||||
val streamSecurity = Utils.arrayFind(streamSecuritys, streamSetting.security)
|
||||
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
|
||||
}
|
||||
@@ -92,113 +199,206 @@ class ServerActivity : BaseActivity() {
|
||||
/**
|
||||
* clear or init server config
|
||||
*/
|
||||
fun clearServer(): Boolean {
|
||||
private fun clearServer(): Boolean {
|
||||
et_remarks.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_alterId.text = Utils.getEditable("64")
|
||||
sp_security.setSelection(0)
|
||||
sp_network.setSelection(0)
|
||||
et_alterId?.text = Utils.getEditable("0")
|
||||
sp_security?.setSelection(0)
|
||||
sp_network?.setSelection(0)
|
||||
|
||||
sp_header_type.setSelection(0)
|
||||
et_request_host.text = null
|
||||
et_path.text = null
|
||||
sp_stream_security.setSelection(0)
|
||||
sp_header_type?.setSelection(0)
|
||||
et_request_host?.text = null
|
||||
et_path?.text = null
|
||||
sp_stream_security?.setSelection(0)
|
||||
sp_allow_insecure?.setSelection(0)
|
||||
et_sni?.text = null
|
||||
|
||||
//et_security.text = null
|
||||
sp_flow?.setSelection(0)
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* save server config
|
||||
*/
|
||||
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.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)) {
|
||||
private fun saveServer(): Boolean {
|
||||
if (TextUtils.isEmpty(et_remarks.text.toString())) {
|
||||
toast(R.string.server_lab_remarks)
|
||||
return false
|
||||
}
|
||||
if (TextUtils.isEmpty(vmess.address)) {
|
||||
if (TextUtils.isEmpty(et_address.text.toString())) {
|
||||
toast(R.string.server_lab_address)
|
||||
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)
|
||||
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)
|
||||
return false
|
||||
}
|
||||
if (TextUtils.isEmpty(vmess.alterId.toString()) || vmess.alterId < 0) {
|
||||
sp_stream_security?.let {
|
||||
if (config.configType == EConfigType.TROJAN && TextUtils.isEmpty(streamSecuritys[it.selectedItemPosition])) {
|
||||
toast(R.string.server_lab_stream_security)
|
||||
return false
|
||||
}
|
||||
}
|
||||
et_alterId?.let {
|
||||
val alterId = Utils.parseInt(it.text.toString())
|
||||
if (alterId < 0) {
|
||||
toast(R.string.server_lab_alterid)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
if (AngConfigManager.addServer(vmess, edit_index) == 0) {
|
||||
config.remarks = et_remarks.text.toString().trim()
|
||||
config.outboundBean?.settings?.vnext?.get(0)?.let { vnext ->
|
||||
saveVnext(vnext, port, config)
|
||||
}
|
||||
config.outboundBean?.settings?.servers?.get(0)?.let { server ->
|
||||
saveServers(server, port, config)
|
||||
}
|
||||
config.outboundBean?.streamSettings?.let {
|
||||
saveStreamSettings(it)
|
||||
}
|
||||
if(config.subscriptionId.isEmpty() && !subscriptionId.isNullOrEmpty()) {
|
||||
config.subscriptionId = subscriptionId!!
|
||||
}
|
||||
|
||||
MmkvManager.encodeServerConfig(editGuid, config)
|
||||
toast(R.string.toast_success)
|
||||
finish()
|
||||
return true
|
||||
}
|
||||
|
||||
private fun saveVnext(vnext: V2rayConfig.OutboundBean.OutSettingsBean.VnextBean, port: Int, config: ServerConfig) {
|
||||
vnext.address = et_address.text.toString().trim()
|
||||
vnext.port = port
|
||||
vnext.users[0].id = et_id.text.toString().trim()
|
||||
if (config.configType == EConfigType.VMESS) {
|
||||
vnext.users[0].alterId = Utils.parseInt(et_alterId?.text.toString())
|
||||
vnext.users[0].security = securitys[sp_security?.selectedItemPosition ?: 0]
|
||||
} else if (config.configType == EConfigType.VLESS) {
|
||||
vnext.users[0].encryption = et_security?.text.toString().trim()
|
||||
if (streamSecuritys[sp_stream_security?.selectedItemPosition ?: 0] == XTLS) {
|
||||
vnext.users[0].flow = flows[sp_flow?.selectedItemPosition ?: 0].ifBlank { DEFAULT_FLOW }
|
||||
} else {
|
||||
toast(R.string.toast_failure)
|
||||
return false
|
||||
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 {
|
||||
allowinsecures[allowInsecureField].toBoolean()
|
||||
}
|
||||
|
||||
streamSetting.populateTlsSettings(streamSecuritys[streamSecurity], allowInsecure, sni, uTlsItems[utlsIndex], alpns[alpnIndex])
|
||||
}
|
||||
|
||||
private fun transportTypes(network: String?): Array<out String> {
|
||||
return if (network == "tcp") {
|
||||
tcpTypes
|
||||
} else if (network == "kcp" || network == "quic") {
|
||||
kcpAndQuicTypes
|
||||
} else if (network == "grpc") {
|
||||
grpcModes
|
||||
} else {
|
||||
arrayOf("---")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* save server config
|
||||
*/
|
||||
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)
|
||||
private fun deleteServer(): Boolean {
|
||||
if (editGuid.isNotEmpty()) {
|
||||
if (editGuid != mainStorage?.decodeString(MmkvManager.KEY_SELECTED_SERVER)) {
|
||||
if (settingsStorage?.decodeBool(AppConfig.PREF_CONFIRM_REMOVE) == true) {
|
||||
AlertDialog.Builder(this).setMessage(R.string.del_config_comfirm)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
MmkvManager.removeServer(editGuid)
|
||||
finish()
|
||||
}
|
||||
.show()
|
||||
} else {
|
||||
toast(R.string.toast_failure)
|
||||
MmkvManager.removeServer(editGuid)
|
||||
finish()
|
||||
}
|
||||
}
|
||||
show()
|
||||
}
|
||||
} else {
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
|
||||
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)
|
||||
val delButton = menu.findItem(R.id.del_config)
|
||||
val saveButton = menu.findItem(R.id.save_config)
|
||||
|
||||
if (edit_index >= 0) {
|
||||
if (editGuid.isNotEmpty()) {
|
||||
if (isRunning) {
|
||||
if (edit_index == configs.index) {
|
||||
del_config?.isVisible = false
|
||||
save_config?.isVisible = false
|
||||
}
|
||||
delButton?.isVisible = false
|
||||
saveButton?.isVisible = false
|
||||
}
|
||||
} else {
|
||||
del_config?.isVisible = false
|
||||
delButton?.isVisible = false
|
||||
}
|
||||
|
||||
return super.onCreateOptionsMenu(menu)
|
||||
|
||||
@@ -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
|
||||
|
||||
import android.content.Intent
|
||||
import android.content.SharedPreferences
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.preference.*
|
||||
import com.v2ray.ang.AngApplication
|
||||
import com.v2ray.ang.BuildConfig
|
||||
//import com.v2ray.ang.InappBuyActivity
|
||||
import com.v2ray.ang.R
|
||||
import android.text.TextUtils
|
||||
import android.view.View
|
||||
import androidx.activity.viewModels
|
||||
import androidx.preference.*
|
||||
import com.v2ray.ang.AppConfig
|
||||
import com.v2ray.ang.extension.defaultDPreference
|
||||
import com.v2ray.ang.extension.onClick
|
||||
import com.v2ray.ang.R
|
||||
import com.v2ray.ang.util.Utils
|
||||
import org.jetbrains.anko.act
|
||||
import org.jetbrains.anko.defaultSharedPreferences
|
||||
import org.jetbrains.anko.startActivity
|
||||
import org.jetbrains.anko.toast
|
||||
import libv2ray.Libv2ray
|
||||
import com.v2ray.ang.viewmodel.SettingsViewModel
|
||||
|
||||
class SettingsActivity : BaseActivity() {
|
||||
companion object {
|
||||
// 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"
|
||||
}
|
||||
private val settingsViewModel: SettingsViewModel by viewModels()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
@@ -54,126 +21,35 @@ class SettingsActivity : BaseActivity() {
|
||||
title = getString(R.string.title_settings)
|
||||
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
|
||||
settingsViewModel.startListenPreferenceChange()
|
||||
}
|
||||
|
||||
class SettingsFragment : PreferenceFragment(), SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
val perAppProxy by lazy { findPreference(PREF_PER_APP_PROXY) as CheckBoxPreference }
|
||||
val sppedEnabled by lazy { findPreference(PREF_SPEED_ENABLED) as CheckBoxPreference }
|
||||
val sniffingEnabled by lazy { findPreference(PREF_SNIFFING_ENABLED) as CheckBoxPreference }
|
||||
val proxySharing by lazy { findPreference(PREF_PROXY_SHARING) as CheckBoxPreference }
|
||||
val domainStrategy by lazy { findPreference(PREF_ROUTING_DOMAIN_STRATEGY) as ListPreference }
|
||||
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 }
|
||||
|
||||
class SettingsFragment : PreferenceFragmentCompat() {
|
||||
private val perAppProxy by lazy { findPreference<CheckBoxPreference>(AppConfig.PREF_PER_APP_PROXY) }
|
||||
private val localDns by lazy { findPreference<CheckBoxPreference>(AppConfig.PREF_LOCAL_DNS_ENABLED) }
|
||||
private val fakeDns by lazy { findPreference<CheckBoxPreference>(AppConfig.PREF_FAKE_DNS_ENABLED) }
|
||||
private val localDnsPort by lazy { findPreference<EditTextPreference>(AppConfig.PREF_LOCAL_DNS_PORT) }
|
||||
private val vpnDns by lazy { findPreference<EditTextPreference>(AppConfig.PREF_VPN_DNS) }
|
||||
// val autoRestart by lazy { findPreference(PREF_AUTO_RESTART) as CheckBoxPreference }
|
||||
|
||||
|
||||
// val socksPort by lazy { findPreference(PREF_SOCKS_PORT) as EditTextPreference }
|
||||
// val httpPort by lazy { findPreference(PREF_HTTP_PORT) as EditTextPreference }
|
||||
|
||||
val routingCustom: Preference by lazy { findPreference(PREF_ROUTING_CUSTOM) }
|
||||
// val donate: Preference by lazy { findPreference(PREF_DONATE) }
|
||||
private val remoteDns by lazy { findPreference<EditTextPreference>(AppConfig.PREF_REMOTE_DNS) }
|
||||
private val domesticDns by lazy { findPreference<EditTextPreference>(AppConfig.PREF_DOMESTIC_DNS) }
|
||||
private val socksPort by lazy { findPreference<EditTextPreference>(AppConfig.PREF_SOCKS_PORT) }
|
||||
private val httpPort by lazy { findPreference<EditTextPreference>(AppConfig.PREF_HTTP_PORT) }
|
||||
private val routingCustom by lazy { findPreference<Preference>(AppConfig.PREF_ROUTING_CUSTOM) }
|
||||
// val licenses: Preference by lazy { findPreference(PREF_LICENSES) }
|
||||
// val feedback: Preference by lazy { findPreference(PREF_FEEDBACK) }
|
||||
// val tgGroup: Preference by lazy { findPreference(PREF_TG_GROUP) }
|
||||
val version: Preference by lazy { findPreference(PREF_VERSION) }
|
||||
|
||||
private fun restartProxy() {
|
||||
Utils.stopVService(activity)
|
||||
Utils.startVService(activity)
|
||||
}
|
||||
private val mode by lazy { findPreference<ListPreference>(AppConfig.PREF_MODE) }
|
||||
|
||||
private fun isRunning(): Boolean {
|
||||
return Utils.isServiceRun(activity, "com.v2ray.ang.service.V2RayVpnService")
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
override fun onCreatePreferences(bundle: Bundle?, s: String?) {
|
||||
addPreferencesFromResource(R.xml.pref_settings)
|
||||
var app = activity.application as AngApplication
|
||||
|
||||
perAppProxy.setOnPreferenceClickListener {
|
||||
if (isRunning()) {
|
||||
Utils.stopVService(activity)
|
||||
routingCustom?.setOnPreferenceClickListener {
|
||||
startActivity(Intent(activity, RoutingSettingsActivity::class.java))
|
||||
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 {
|
||||
// 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 ->
|
||||
// socksPort.summary = any as String
|
||||
// true
|
||||
// }
|
||||
// httpPort.setOnPreferenceChangeListener { preference, any ->
|
||||
// httpPort.summary = any as String
|
||||
// true
|
||||
// }
|
||||
remoteDns?.setOnPreferenceChangeListener { _, any ->
|
||||
// remoteDns.summary = any as String
|
||||
val nval = any as String
|
||||
remoteDns?.summary = if (nval == "") AppConfig.DNS_AGENT else nval
|
||||
true
|
||||
}
|
||||
domesticDns?.setOnPreferenceChangeListener { _, any ->
|
||||
// domesticDns.summary = any as String
|
||||
val nval = any as String
|
||||
domesticDns?.summary = if (nval == "") AppConfig.DNS_DIRECT else nval
|
||||
true
|
||||
}
|
||||
|
||||
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() {
|
||||
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)
|
||||
remoteDns.summary = defaultSharedPreferences.getString(PREF_REMOTE_DNS, "")
|
||||
domesticDns.summary = defaultSharedPreferences.getString(PREF_DOMESTIC_DNS, "")
|
||||
localDnsPort?.summary = defaultSharedPreferences.getString(AppConfig.PREF_LOCAL_DNS_PORT, AppConfig.PORT_LOCAL_DNS)
|
||||
socksPort?.summary = defaultSharedPreferences.getString(AppConfig.PREF_SOCKS_PORT, AppConfig.PORT_SOCKS)
|
||||
httpPort?.summary = defaultSharedPreferences.getString(AppConfig.PREF_HTTP_PORT, AppConfig.PORT_HTTP)
|
||||
|
||||
if (remoteDns.summary == "") {
|
||||
remoteDns.summary = AppConfig.DNS_AGENT
|
||||
if (TextUtils.isEmpty(remoteDnsString)) {
|
||||
remoteDnsString = AppConfig.DNS_AGENT
|
||||
}
|
||||
|
||||
if ( domesticDns.summary == "") {
|
||||
domesticDns.summary = AppConfig.DNS_DIRECT
|
||||
if (TextUtils.isEmpty(domesticDns?.summary)) {
|
||||
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")
|
||||
// lanconnPort.summary = defaultSharedPreferences.getString(PREF_HTTP_PORT, "")
|
||||
|
||||
defaultSharedPreferences.registerOnSharedPreferenceChangeListener(this)
|
||||
if (TextUtils.isEmpty(localDnsPort?.summary)) {
|
||||
localDnsPort?.summary = AppConfig.PORT_LOCAL_DNS
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
super.onStop()
|
||||
defaultSharedPreferences.unregisterOnSharedPreferenceChangeListener(this)
|
||||
if (TextUtils.isEmpty(socksPort?.summary)) {
|
||||
socksPort?.summary = AppConfig.PORT_SOCKS
|
||||
}
|
||||
|
||||
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String?) {
|
||||
when (key) {
|
||||
// PREF_AUTO_RESTART ->
|
||||
// act.defaultDPreference.setPrefBoolean(key, sharedPreferences.getBoolean(key, false))
|
||||
|
||||
PREF_PER_APP_PROXY ->
|
||||
act.defaultDPreference.setPrefBoolean(key, sharedPreferences.getBoolean(key, false))
|
||||
if (TextUtils.isEmpty(httpPort?.summary)) {
|
||||
httpPort?.summary = AppConfig.PORT_HTTP
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateMode(mode: String?) {
|
||||
val defaultSharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireActivity())
|
||||
val vpn = mode == "VPN"
|
||||
perAppProxy?.isEnabled = vpn
|
||||
perAppProxy?.isChecked = PreferenceManager.getDefaultSharedPreferences(requireActivity())
|
||||
.getBoolean(AppConfig.PREF_PER_APP_PROXY, false)
|
||||
localDns?.isEnabled = vpn
|
||||
fakeDns?.isEnabled = vpn
|
||||
localDnsPort?.isEnabled = vpn
|
||||
vpnDns?.isEnabled = vpn
|
||||
if (vpn) {
|
||||
updateLocalDns(defaultSharedPreferences.getBoolean(AppConfig.PREF_LOCAL_DNS_ENABLED, false))
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateLocalDns(enabled: Boolean) {
|
||||
fakeDns?.isEnabled = enabled
|
||||
localDnsPort?.isEnabled = enabled
|
||||
vpnDns?.isEnabled = !enabled
|
||||
}
|
||||
}
|
||||
|
||||
fun onModeHelpClicked(view: View) {
|
||||
Utils.openUri(this, AppConfig.v2rayNGWikiMode)
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user