Compare commits
180 Commits
1.5.2-v2fl
...
1.7.26
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2fb5d05040 | ||
|
|
221b251bbf | ||
|
|
1a90e77d56 | ||
|
|
0ca1c697c7 | ||
|
|
3e9a6b5d5c | ||
|
|
77a1d77753 | ||
|
|
8732ef399c | ||
|
|
f2720ac00b | ||
|
|
1794ae4759 | ||
|
|
866ceae75c | ||
|
|
349cac092f | ||
|
|
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 |
4
.gitignore
vendored
4
.gitignore
vendored
@@ -1,7 +1,3 @@
|
||||
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
|
||||
*.dat
|
||||
*.jks
|
||||
V2rayNG/app/release/output.json
|
||||
|
||||
@@ -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,65 +0,0 @@
|
||||
package CoreI
|
||||
|
||||
import (
|
||||
v2core "v2ray.com/core"
|
||||
)
|
||||
|
||||
type Status struct {
|
||||
IsRunning bool
|
||||
IsTRunning bool
|
||||
PackageName string
|
||||
PackageCodePath string
|
||||
|
||||
Vpoint v2core.Server
|
||||
}
|
||||
|
||||
func CheckVersion() int {
|
||||
return 22
|
||||
}
|
||||
|
||||
func (v *Status) GetDataDir() string {
|
||||
return v.PackageName
|
||||
}
|
||||
|
||||
func (v *Status) GetApp(name string) string {
|
||||
return v.PackageCodePath + 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,31 +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
|
||||
|
||||
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 fetchDep
|
||||
@echo DONE
|
||||
@@ -1,79 +0,0 @@
|
||||
package Escort
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
|
||||
"log"
|
||||
|
||||
"github.com/2dust/AndroidLibV2rayLite/CoreI"
|
||||
)
|
||||
|
||||
func (v *Escorting) EscortRun(proc string, pt []string, additionalEnv string) {
|
||||
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 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,43 +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/../V2rayNG/app/src/main/jniLibs/armeabi-v7a/libtun2socks.so
|
||||
install -v -m755 libs/arm64-v8a/tun2socks $__dir/../V2rayNG/app/src/main/jniLibs/arm64-v8a/libtun2socks.so
|
||||
install -v -m755 libs/x86/tun2socks $__dir/../V2rayNG/app/src/main/jniLibs/x86/libtun2socks.so
|
||||
install -v -m755 libs/x86_64/tun2socks $__dir/../V2rayNG/app/src/main/jniLibs/x86_64/libtun2socks.so
|
||||
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,260 +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"
|
||||
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
|
||||
PackageCodePath string
|
||||
DomainName string
|
||||
ConfigureFileContent string
|
||||
EnableLocalDNS bool
|
||||
ForwardIpv6 bool
|
||||
ProxyOnly 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
|
||||
}
|
||||
|
||||
/*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
|
||||
v.status.PackageCodePath = v.PackageCodePath
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
// 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 {
|
||||
v.shutdownInit()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
//Delegate Funcation
|
||||
func (v *V2RayPoint) GetIsRunning() bool {
|
||||
return v.status.IsRunning
|
||||
}
|
||||
|
||||
func (v *V2RayPoint) GetIsTRunning() bool {
|
||||
return v.status.IsTRunning
|
||||
}
|
||||
|
||||
//Delegate Funcation
|
||||
func (v V2RayPoint) QueryStats(tag string, direct string) int64 {
|
||||
if v.statsManager == nil {
|
||||
return 0
|
||||
}
|
||||
counter := v.statsManager.GetCounter(fmt.Sprintf("outbound>>>%s>>>traffic>>>%s", tag, direct))
|
||||
if counter == nil {
|
||||
return 0
|
||||
}
|
||||
return counter.Set(0)
|
||||
}
|
||||
|
||||
func (v *V2RayPoint) shutdownInit() {
|
||||
close(v.closeChan)
|
||||
v.statsManager = nil
|
||||
v.status.Vpoint.Close()
|
||||
v.status.Vpoint = nil
|
||||
v.status.IsRunning = false
|
||||
|
||||
v.escorter.EscortingDown()
|
||||
|
||||
v.SupportSet.Shutdown()
|
||||
v.SupportSet.OnEmitStatus(0, "Closed")
|
||||
}
|
||||
|
||||
func (v *V2RayPoint) pointloop() error {
|
||||
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")
|
||||
|
||||
v.status.IsTRunning = false
|
||||
if !v.ProxyOnly {
|
||||
if err := v.runTun2socks(); err != nil {
|
||||
log.Println(err)
|
||||
return err
|
||||
}
|
||||
v.status.IsTRunning = true
|
||||
|
||||
log.Printf("EnableLocalDNS: %v\nForwardIpv6: %v\nDomainName: %s",
|
||||
v.EnableLocalDNS,
|
||||
v.ForwardIpv6,
|
||||
v.DomainName)
|
||||
}
|
||||
|
||||
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 {
|
||||
v.escorter.EscortingUp()
|
||||
go v.escorter.EscortRun(
|
||||
v.status.GetApp("libtun2socks.so"),
|
||||
v.status.GetTun2socksArgs(v.EnableLocalDNS, v.ForwardIpv6), "")
|
||||
|
||||
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
|
||||
@@ -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)}
|
||||
}
|
||||
}
|
||||
30
README.md
30
README.md
@@ -1,34 +1,36 @@
|
||||
# v2rayNG
|
||||
|
||||
A V2Ray client for Android
|
||||
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/jelly-bean#android-4.2)
|
||||
[](https://kotlinlang.org)
|
||||
[](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
|
||||
v2rayNG release already embedded domain file `geoip.dat` and `geosite.dat`. However it is (probably) not the latest and not the most complete list.
|
||||
For power user, the embedded files can be easily replaced with the following steps:
|
||||
1. Launch v2rayNG (v1.4.9+)
|
||||
2. Find existing geoip.dat and geosite.dat in `Android/data/com.v2ray.ang/files/assets` (path may differ on some Android device)
|
||||
3. Replace them with the latest [domain list](https://github.com/v2fly/domain-list-community) and [ip list](https://github.com/v2fly/geoip)
|
||||
4. Enhanced version can be found in this [repo](https://github.com/Loyalsoldier/v2ray-rules-dat) (recommend to use `geosite:geolocation-!cn` for proxy dns and routing)
|
||||
5. It is also 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)
|
||||
- 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)
|
||||
|
||||
#### See more in our [wiki](https://github.com/2dust/v2rayNG/wiki)
|
||||
### 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 under AndroidLibV2rayLite folder. 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/)
|
||||
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, with minimum Android 5.0
|
||||
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 Integer.parseInt("$compileSdkVer")
|
||||
buildToolsVersion buildToolsVer
|
||||
buildToolsVersion "$buildToolsVer"
|
||||
|
||||
compileOptions {
|
||||
targetCompatibility = "8"
|
||||
@@ -13,11 +15,28 @@ android {
|
||||
|
||||
defaultConfig {
|
||||
applicationId "com.v2ray.ang"
|
||||
minSdkVersion 17
|
||||
minSdkVersion 21
|
||||
targetSdkVersion Integer.parseInt("$targetSdkVer")
|
||||
multiDexEnabled true
|
||||
versionCode 212
|
||||
versionName "1.0.2"
|
||||
versionCode 486
|
||||
versionName "1.7.26"
|
||||
}
|
||||
|
||||
if (props["sign"]) {
|
||||
signingConfigs {
|
||||
release {
|
||||
storeFile file("../key.jks")
|
||||
keyAlias 'ang'
|
||||
keyPassword '123456'
|
||||
storePassword '123456'
|
||||
}
|
||||
debug {
|
||||
storeFile file("../key.jks")
|
||||
keyAlias 'ang'
|
||||
keyPassword '123456'
|
||||
storePassword '123456'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
@@ -25,17 +44,28 @@ android {
|
||||
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 {
|
||||
@@ -57,62 +87,65 @@ android {
|
||||
android.applicationVariants.all { variant ->
|
||||
// assign different version code for each output
|
||||
variant.outputs.each { output ->
|
||||
output.versionCodeOverride =
|
||||
output.outputFileName = "v2rayNG_" + variant.versionName + "_" + output.getFilter(com.android.build.OutputFile.ABI) + ".apk"
|
||||
|
||||
output.versionCodeOverride =
|
||||
project.ext.versionCodes.get(output.getFilter(com.android.build.OutputFile.ABI), 0) *
|
||||
1000000 + android.defaultConfig.versionCode
|
||||
}
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
viewBinding true
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation fileTree(include: ['*.jar'], dir: 'libs')
|
||||
testImplementation 'junit:junit:4.13'
|
||||
implementation project(':dpreference')
|
||||
implementation fileTree(dir: 'libs', include: ['*.aar', '*.jar'], exclude: [])
|
||||
testImplementation 'junit:junit:4.13.2'
|
||||
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion"
|
||||
implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlinVersion"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.9"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9"
|
||||
// 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 'android.arch.lifecycle:extensions:1.1.1'
|
||||
implementation 'android.arch.lifecycle:livedata:1.1.1'
|
||||
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'
|
||||
|
||||
// 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"
|
||||
implementation "com.android.support:multidex:1.0.3"
|
||||
implementation 'com.android.support.constraint:constraint-layout:2.0.1'
|
||||
//kotlin
|
||||
implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlinVersion"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2"
|
||||
|
||||
// DSL
|
||||
implementation 'com.google.code.gson:gson:2.8.6'
|
||||
implementation 'com.tencent:mmkv-static:1.2.12'
|
||||
implementation 'com.google.code.gson:gson:2.8.9'
|
||||
implementation 'io.reactivex:rxjava:1.3.4'
|
||||
implementation 'io.reactivex:rxandroid:1.2.1'
|
||||
implementation 'com.tbruyelle.rxpermissions:rxpermissions:0.9.4@aar'
|
||||
implementation 'me.dm7.barcodescanner:core:1.9.8'
|
||||
implementation 'me.dm7.barcodescanner:zxing:1.9.8'
|
||||
implementation 'com.github.jorgecastilloprz:fabprogresscircle:1.01@aar'
|
||||
|
||||
implementation(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.
58
V2rayNG/app/proguard-rules.pro
vendored
58
V2rayNG/app/proguard-rules.pro
vendored
@@ -1,58 +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 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.** { *;}
|
||||
|
||||
@@ -4,27 +4,24 @@
|
||||
package="com.v2ray.ang">
|
||||
|
||||
<supports-screens
|
||||
android:anyDensity="true"
|
||||
android:smallScreens="true"
|
||||
android:normalScreens="true"
|
||||
android:largeScreens="true"
|
||||
android:xlargeScreens="true"/>
|
||||
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"/>
|
||||
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" /> -->
|
||||
|
||||
@@ -34,21 +31,17 @@
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:supportsRtl="true"
|
||||
android:extractNativeLibs="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>
|
||||
@@ -57,35 +50,66 @@
|
||||
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:name=".ui.SubEditActivity" />
|
||||
<activity android:name=".ui.ScScannerActivity" />
|
||||
<activity
|
||||
android:exported="false"
|
||||
android:name=".ui.SubSettingActivity" />
|
||||
<activity
|
||||
android:exported="false"
|
||||
android:name=".ui.UserAssetActivity" />
|
||||
|
||||
<activity
|
||||
android:exported="false"
|
||||
android:name=".ui.SubEditActivity" />
|
||||
<activity
|
||||
android:exported="false"
|
||||
android:name=".ui.ScScannerActivity" />
|
||||
<activity
|
||||
android:exported="false"
|
||||
android:name=".ui.ScSwitchActivity"
|
||||
android:excludeFromRecents="true"
|
||||
android:process=":RunSoLibV2RayDaemon"
|
||||
android:theme="@style/AppTheme.NoActionBar.Translucent" />
|
||||
|
||||
<activity
|
||||
android:exported="true"
|
||||
android:name=".ui.UrlSchemeActivity">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:mimeType="text/plain" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
<category android:name="android.intent.category.BROWSABLE"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<data android:scheme="v2rayng"
|
||||
android:host="install-config" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<service
|
||||
android:name=".service.V2RayVpnService"
|
||||
android:enabled="true"
|
||||
@@ -107,7 +131,14 @@
|
||||
android:process=":RunSoLibV2RayDaemon">
|
||||
</service>
|
||||
|
||||
<receiver android:name=".receiver.WidgetProvider"
|
||||
<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"
|
||||
@@ -120,8 +151,9 @@
|
||||
</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:process=":RunSoLibV2RayDaemon">
|
||||
@@ -131,6 +163,7 @@
|
||||
</service>
|
||||
<!-- =====================Tasker===================== -->
|
||||
<activity
|
||||
android:exported="true"
|
||||
android:name=".ui.TaskerActivity"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name">
|
||||
@@ -139,7 +172,9 @@
|
||||
</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" />
|
||||
|
||||
@@ -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,
|
||||
@@ -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}.
|
||||
|
||||
@@ -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,7 +112,7 @@ 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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,21 +1,17 @@
|
||||
package com.v2ray.ang
|
||||
|
||||
import android.support.multidex.MultiDexApplication
|
||||
import android.support.v7.preference.PreferenceManager
|
||||
import com.v2ray.ang.util.AngConfigManager
|
||||
import me.dozen.dpreference.DPreference
|
||||
import androidx.multidex.MultiDexApplication
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.tencent.mmkv.MMKV
|
||||
|
||||
class AngApplication : MultiDexApplication() {
|
||||
companion object {
|
||||
const val PREF_LAST_VERSION = "pref_last_version"
|
||||
}
|
||||
|
||||
var 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()
|
||||
|
||||
@@ -27,6 +23,6 @@ class AngApplication : MultiDexApplication() {
|
||||
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,18 +6,43 @@ package com.v2ray.ang
|
||||
*/
|
||||
object AppConfig {
|
||||
const val ANG_PACKAGE = "com.v2ray.ang"
|
||||
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_CURR_CONFIG_OUTBOUND_TAGS = "pref_v2ray_config_outbound_tags"
|
||||
const val PREF_INAPP_BUY_IS_PREMIUM = "pref_inapp_buy_is_premium"
|
||||
const val PREF_MODE = "pref_mode"
|
||||
const val DIR_ASSETS = "assets"
|
||||
|
||||
// legacy
|
||||
const val ANG_CONFIG = "ang_config"
|
||||
const val PREF_INAPP_BUY_IS_PREMIUM = "pref_inapp_buy_is_premium"
|
||||
const val PREF_ROUTING_CUSTOM = "pref_routing_custom"
|
||||
|
||||
// Preferences mapped to MMKV
|
||||
const val PREF_MODE = "pref_mode"
|
||||
const val PREF_SPEED_ENABLED = "pref_speed_enabled"
|
||||
const val PREF_SNIFFING_ENABLED = "pref_sniffing_enabled"
|
||||
const val PREF_PROXY_SHARING = "pref_proxy_sharing_enabled"
|
||||
const val PREF_LOCAL_DNS_ENABLED = "pref_local_dns_enabled"
|
||||
const val PREF_FAKE_DNS_ENABLED = "pref_fake_dns_enabled"
|
||||
const val PREF_VPN_DNS = "pref_vpn_dns"
|
||||
const val PREF_REMOTE_DNS = "pref_remote_dns"
|
||||
const val PREF_DOMESTIC_DNS = "pref_domestic_dns"
|
||||
const val PREF_LOCAL_DNS_PORT = "pref_local_dns_port"
|
||||
const val PREF_ALLOW_INSECURE = "pref_allow_insecure"
|
||||
const val PREF_SOCKS_PORT = "pref_socks_port"
|
||||
const val PREF_HTTP_PORT = "pref_http_port"
|
||||
const val PREF_LOGLEVEL = "pref_core_loglevel"
|
||||
const val PREF_LANGUAGE = "pref_language"
|
||||
const val PREF_PREFER_IPV6 = "pref_prefer_ipv6"
|
||||
const val PREF_ROUTING_DOMAIN_STRATEGY = "pref_routing_domain_strategy"
|
||||
const val PREF_ROUTING_MODE = "pref_routing_mode"
|
||||
const val PREF_V2RAY_ROUTING_AGENT = "pref_v2ray_routing_agent"
|
||||
const val PREF_V2RAY_ROUTING_DIRECT = "pref_v2ray_routing_direct"
|
||||
const val PREF_V2RAY_ROUTING_BLOCKED = "pref_v2ray_routing_blocked"
|
||||
const val PREF_PER_APP_PROXY = "pref_per_app_proxy"
|
||||
const val PREF_PER_APP_PROXY_SET = "pref_per_app_proxy_set"
|
||||
const val PREF_BYPASS_APPS = "pref_bypass_apps"
|
||||
const val PREF_CONFIRM_REMOVE = "pref_confirm_remove"
|
||||
|
||||
const val HTTP_PROTOCOL: String = "http://"
|
||||
const val HTTPS_PROTOCOL: String = "https://"
|
||||
|
||||
const val VMESS_PROTOCOL: String = "vmess://"
|
||||
const val SS_PROTOCOL: String = "ss://"
|
||||
const val SOCKS_PROTOCOL: String = "socks://"
|
||||
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"
|
||||
@@ -28,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"
|
||||
@@ -39,11 +61,16 @@ object AppConfig {
|
||||
const val v2rayCustomRoutingListUrl = "https://raw.githubusercontent.com/2dust/v2rayCustomRoutingList/master/"
|
||||
const val v2rayNGIssues = "https://github.com/2dust/v2rayNG/issues"
|
||||
const val v2rayNGWikiMode = "https://github.com/2dust/v2rayNG/wiki/Mode"
|
||||
const val promotionUrl = "https://1.2345345.xyz/ads.html"
|
||||
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
|
||||
@@ -54,4 +81,9 @@ object AppConfig {
|
||||
const val MSG_STATE_STOP = 4
|
||||
const val MSG_STATE_STOP_SUCCESS = 41
|
||||
const val MSG_STATE_RESTART = 5
|
||||
const val MSG_MEASURE_DELAY = 6
|
||||
const val MSG_MEASURE_DELAY_SUCCESS = 61
|
||||
const val MSG_MEASURE_CONFIG = 7
|
||||
const val MSG_MEASURE_CONFIG_SUCCESS = 71
|
||||
const val MSG_MEASURE_CONFIG_CANCEL = 72
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
package com.v2ray.ang.dto
|
||||
|
||||
enum class EConfigType(val value: Int) {
|
||||
VMESS(1),
|
||||
CUSTOM(2),
|
||||
SHADOWSOCKS(3),
|
||||
SOCKS(4);
|
||||
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,159 +1,451 @@
|
||||
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 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 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,
|
||||
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 path: String = "",
|
||||
var headers: HeadersBean = HeadersBean()) {
|
||||
var headers: HeadersBean = HeadersBean(),
|
||||
val maxEarlyData: Int? = null,
|
||||
val useBrowserForwarding: Boolean? = null,
|
||||
val acceptProxyProtocol: Boolean? = null) {
|
||||
data class HeadersBean(var Host: String = "")
|
||||
}
|
||||
|
||||
data class HttpSettingsBean(var host: List<String> = ArrayList(),
|
||||
var path: String = "")
|
||||
|
||||
data class TlsSettingsBean(var allowInsecure: Boolean = 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",
|
||||
var key: String = "",
|
||||
var header: HeaderBean = HeaderBean()) {
|
||||
data class HeaderBean(var type: String = "none")
|
||||
}
|
||||
|
||||
data class GrpcSettingsBean(var serviceName: String = "",
|
||||
var multiMode: Boolean? = null)
|
||||
|
||||
fun populateTransportSettings(transport: String, headerType: String?, host: String?, path: String?, seed: String?,
|
||||
quicSecurity: String?, key: String?, mode: String?, serviceName: String?): String {
|
||||
var sni = ""
|
||||
network = transport
|
||||
when (network) {
|
||||
"tcp" -> {
|
||||
val tcpSetting = TcpSettingsBean()
|
||||
if (headerType == HTTP) {
|
||||
tcpSetting.header.type = HTTP
|
||||
if (!TextUtils.isEmpty(host) || !TextUtils.isEmpty(path)) {
|
||||
val requestObj = TcpSettingsBean.HeaderBean.RequestBean()
|
||||
requestObj.headers.Host = (host ?: "").split(",").map { it.trim() }.filter { it.isNotEmpty() }
|
||||
requestObj.path = (path ?: "").split(",").map { it.trim() }.filter { it.isNotEmpty() }
|
||||
tcpSetting.header.request = requestObj
|
||||
sni = requestObj.headers.Host.getOrNull(0) ?: sni
|
||||
}
|
||||
} else {
|
||||
tcpSetting.header.type = "none"
|
||||
sni = host ?: ""
|
||||
}
|
||||
tcpSettings = tcpSetting
|
||||
}
|
||||
"kcp" -> {
|
||||
val kcpsetting = KcpSettingsBean()
|
||||
kcpsetting.header.type = headerType ?: "none"
|
||||
if (seed.isNullOrEmpty()) {
|
||||
kcpsetting.seed = null
|
||||
} else {
|
||||
kcpsetting.seed = seed
|
||||
}
|
||||
kcpSettings = kcpsetting
|
||||
}
|
||||
"ws" -> {
|
||||
val wssetting = WsSettingsBean()
|
||||
wssetting.headers.Host = host ?: ""
|
||||
sni = wssetting.headers.Host
|
||||
wssetting.path = path ?: "/"
|
||||
wsSettings = wssetting
|
||||
}
|
||||
"h2", "http" -> {
|
||||
network = "h2"
|
||||
val h2Setting = HttpSettingsBean()
|
||||
h2Setting.host = (host ?: "").split(",").map { it.trim() }.filter { it.isNotEmpty() }
|
||||
sni = h2Setting.host.getOrNull(0) ?: sni
|
||||
h2Setting.path = path ?: "/"
|
||||
httpSettings = h2Setting
|
||||
}
|
||||
"quic" -> {
|
||||
val quicsetting = QuicSettingBean()
|
||||
quicsetting.security = quicSecurity ?: "none"
|
||||
quicsetting.key = key ?: ""
|
||||
quicsetting.header.type = headerType ?: "none"
|
||||
quicSettings = quicsetting
|
||||
}
|
||||
"grpc" -> {
|
||||
val grpcSetting = GrpcSettingsBean()
|
||||
grpcSetting.multiMode = mode == "multi"
|
||||
grpcSetting.serviceName = serviceName ?: ""
|
||||
sni = host ?: ""
|
||||
grpcSettings = grpcSetting
|
||||
}
|
||||
}
|
||||
return sni
|
||||
}
|
||||
|
||||
fun populateTlsSettings(streamSecurity: String, allowInsecure: Boolean, sni: String, fingerprint: String?, alpns: String?) {
|
||||
security = streamSecurity
|
||||
val tlsSetting = TlsSettingsBean(
|
||||
allowInsecure = allowInsecure,
|
||||
serverName = sni,
|
||||
fingerprint = fingerprint,
|
||||
alpn = if (alpns.isNullOrEmpty()) null else alpns.split(",").map { it.trim() }.filter { it.isNotEmpty() }
|
||||
)
|
||||
if (security == TLS) {
|
||||
tlsSettings = tlsSetting
|
||||
xtlsSettings = null
|
||||
} else if (security == XTLS) {
|
||||
tlsSettings = null
|
||||
xtlsSettings = tlsSetting
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class MuxBean(var enabled: Boolean)
|
||||
data class MuxBean(var enabled: Boolean, var concurrency: Int = 8)
|
||||
|
||||
fun getServerAddress(): String? {
|
||||
if (protocol.equals(EConfigType.VMESS.name.toLowerCase())) {
|
||||
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.toLowerCase()) || protocol.equals(EConfigType.SOCKS.name.toLowerCase())) {
|
||||
} 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.toLowerCase())) {
|
||||
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.toLowerCase()) || protocol.equals(EConfigType.SOCKS.name.toLowerCase())) {
|
||||
} else if (protocol.equals(EConfigType.SHADOWSOCKS.name, true)
|
||||
|| protocol.equals(EConfigType.SOCKS.name, true)
|
||||
|| protocol.equals(EConfigType.TROJAN.name, true)) {
|
||||
return settings?.servers?.get(0)?.port
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun getPassword(): String? {
|
||||
if (protocol.equals(EConfigType.VMESS.name, true)
|
||||
|| protocol.equals(EConfigType.VLESS.name, true)) {
|
||||
return settings?.vnext?.get(0)?.users?.get(0)?.id
|
||||
} else if (protocol.equals(EConfigType.SHADOWSOCKS.name, true)
|
||||
|| protocol.equals(EConfigType.TROJAN.name, true)) {
|
||||
return settings?.servers?.get(0)?.password
|
||||
} else if (protocol.equals(EConfigType.SOCKS.name, true)) {
|
||||
return settings?.servers?.get(0)?.users?.get(0)?.pass
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun getSecurityEncryption(): String? {
|
||||
return when {
|
||||
protocol.equals(EConfigType.VMESS.name, true) -> settings?.vnext?.get(0)?.users?.get(0)?.security
|
||||
protocol.equals(EConfigType.VLESS.name, true) -> settings?.vnext?.get(0)?.users?.get(0)?.encryption
|
||||
protocol.equals(EConfigType.SHADOWSOCKS.name, true) -> settings?.servers?.get(0)?.method
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
fun getTransportSettingDetails(): List<String>? {
|
||||
if (protocol.equals(EConfigType.VMESS.name, true)
|
||||
|| protocol.equals(EConfigType.VLESS.name, true)
|
||||
|| protocol.equals(EConfigType.TROJAN.name, true)) {
|
||||
val transport = streamSettings?.network ?: return null
|
||||
return when (transport) {
|
||||
"tcp" -> {
|
||||
val tcpSetting = streamSettings?.tcpSettings ?: return null
|
||||
listOf(tcpSetting.header.type,
|
||||
tcpSetting.header.request?.headers?.Host?.joinToString().orEmpty(),
|
||||
tcpSetting.header.request?.path?.joinToString().orEmpty())
|
||||
}
|
||||
"kcp" -> {
|
||||
val kcpSetting = streamSettings?.kcpSettings ?: return null
|
||||
listOf(kcpSetting.header.type,
|
||||
"",
|
||||
kcpSetting.seed.orEmpty())
|
||||
}
|
||||
"ws" -> {
|
||||
val wsSetting = streamSettings?.wsSettings ?: return null
|
||||
listOf("",
|
||||
wsSetting.headers.Host,
|
||||
wsSetting.path)
|
||||
}
|
||||
"h2" -> {
|
||||
val h2Setting = streamSettings?.httpSettings ?: return null
|
||||
listOf("",
|
||||
h2Setting.host.joinToString(),
|
||||
h2Setting.path)
|
||||
}
|
||||
"quic" -> {
|
||||
val quicSetting = streamSettings?.quicSettings ?: return null
|
||||
listOf(quicSetting.header.type,
|
||||
quicSetting.security,
|
||||
quicSetting.key)
|
||||
}
|
||||
"grpc" -> {
|
||||
val grpcSetting = streamSettings?.grpcSettings ?: return null
|
||||
listOf(if (grpcSetting.multiMode == true) "multi" else "gun",
|
||||
"",
|
||||
grpcSetting.serviceName)
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
//data class DnsBean(var servers: List<String>)
|
||||
data class DnsBean(var servers: List<Any>?=null,
|
||||
var hosts: Map<String, String>?=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 handshake: Int? = null,
|
||||
var connIdle: Int? = null,
|
||||
var uplinkOnly: Int? = null,
|
||||
var downlinkOnly: Int? = null,
|
||||
val statsUserUplink: Boolean? = null,
|
||||
val statsUserDownlink: Boolean? = null,
|
||||
var bufferSize: Int? = null)
|
||||
}
|
||||
|
||||
data class FakednsBean(var ipPool: String = "198.18.0.0/15",
|
||||
var poolSize: Int = 10000) // roughly 10 times smaller than total ip pool
|
||||
|
||||
fun getProxyOutbound(): OutboundBean? {
|
||||
outbounds.forEach { outbound ->
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -4,8 +4,9 @@ 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
|
||||
|
||||
/**
|
||||
@@ -15,26 +16,19 @@ import java.net.URLConnection
|
||||
val Context.v2RayApplication: AngApplication
|
||||
get() = applicationContext as AngApplication
|
||||
|
||||
// Usage note: DPreference use Android ContentProvider to redirect multi process access to main process.
|
||||
// Currently, RunSoLibV2RayDaemon process will run proxy core, keep minimum configuration and long running
|
||||
// in the background, support toggle on/off. That means it should NOT use DPreference after the initial
|
||||
// creation and setup of the service
|
||||
val Context.defaultDPreference: DPreference
|
||||
get() = v2RayApplication.defaultDPreference
|
||||
|
||||
inline fun Context.toast(message: Int): Toast = Toast
|
||||
fun Context.toast(message: Int): Toast = ToastCompat
|
||||
.makeText(this, message, Toast.LENGTH_SHORT)
|
||||
.apply {
|
||||
show()
|
||||
}
|
||||
|
||||
inline fun Context.toast(message: CharSequence): Toast = Toast
|
||||
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
|
||||
@@ -80,4 +74,7 @@ private fun Float.toShortString(): String {
|
||||
}
|
||||
|
||||
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 {
|
||||
@@ -23,7 +28,8 @@ class TaskerReceiver : BroadcastReceiver() {
|
||||
if (guid == AppConfig.TASKER_DEFAULT_GUID) {
|
||||
Utils.startVServiceFromToggle(context)
|
||||
} else {
|
||||
Utils.startVService(context, guid)
|
||||
mainStorage?.encode(MmkvManager.KEY_SELECTED_SERVER, guid)
|
||||
V2RayServiceManager.startV2Ray(context)
|
||||
}
|
||||
} else {
|
||||
Utils.stopVService(context)
|
||||
|
||||
@@ -6,6 +6,7 @@ import android.appwidget.AppWidgetProvider
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.widget.RemoteViews
|
||||
import com.v2ray.ang.R
|
||||
import com.v2ray.ang.AppConfig
|
||||
@@ -21,16 +22,33 @@ class WidgetProvider : AppWidgetProvider() {
|
||||
updateWidgetBackground(context, appWidgetManager, appWidgetIds, V2RayServiceManager.v2rayPoint.isRunning)
|
||||
}
|
||||
|
||||
|
||||
private fun updateWidgetBackground(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray, isRunning: Boolean) {
|
||||
val remoteViews = RemoteViews(context.packageName, R.layout.widget_switch)
|
||||
val intent = Intent(context, WidgetProvider::class.java)
|
||||
intent.action = AppConfig.BROADCAST_ACTION_WIDGET_CLICK
|
||||
val pendingIntent = PendingIntent.getBroadcast(context, R.id.layout_switch, intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||
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)
|
||||
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)
|
||||
remoteViews.setInt(
|
||||
R.id.layout_switch,
|
||||
"setBackgroundResource",
|
||||
R.drawable.ic_rounded_corner_grey
|
||||
)
|
||||
}
|
||||
|
||||
for (appWidgetId in appWidgetIds) {
|
||||
@@ -50,15 +68,16 @@ class WidgetProvider : AppWidgetProvider() {
|
||||
Utils.startVServiceFromToggle(context)
|
||||
}
|
||||
} else if (AppConfig.BROADCAST_ACTION_ACTIVITY == intent.action) {
|
||||
val manager = AppWidgetManager.getInstance(context)
|
||||
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)
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,11 +22,11 @@ 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 = V2RayServiceManager.currentConfigName
|
||||
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()
|
||||
|
||||
@@ -5,10 +5,9 @@ import android.app.Service
|
||||
interface ServiceControl {
|
||||
fun getService(): Service
|
||||
|
||||
fun startService(parameters: String)
|
||||
fun startService()
|
||||
|
||||
fun stopService()
|
||||
|
||||
fun vpnProtect(socket: Int): Boolean
|
||||
|
||||
}
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
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 {
|
||||
@@ -25,7 +30,7 @@ class V2RayProxyOnlyService : Service(), ServiceControl {
|
||||
return this
|
||||
}
|
||||
|
||||
override fun startService(parameters: String) {
|
||||
override fun startService() {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
@@ -40,4 +45,12 @@ class V2RayProxyOnlyService : Service(), ServiceControl {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,30 +1,32 @@
|
||||
package com.v2ray.ang.service
|
||||
|
||||
import android.app.Notification
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
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.support.annotation.RequiresApi
|
||||
import android.support.v4.app.NotificationCompat
|
||||
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.extension.defaultDPreference
|
||||
import com.v2ray.ang.dto.ServerConfig
|
||||
import com.v2ray.ang.extension.toSpeedString
|
||||
import com.v2ray.ang.extension.toast
|
||||
import com.v2ray.ang.extension.v2RayApplication
|
||||
import com.v2ray.ang.ui.MainActivity
|
||||
import com.v2ray.ang.ui.SettingsActivity
|
||||
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
|
||||
@@ -39,20 +41,18 @@ object V2RayServiceManager {
|
||||
private const val NOTIFICATION_PENDING_INTENT_STOP_V2RAY = 1
|
||||
private const val NOTIFICATION_ICON_THRESHOLD = 3000
|
||||
|
||||
val v2rayPoint: V2RayPoint = Libv2ray.newV2RayPoint(V2RayCallback())
|
||||
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
|
||||
val context = value?.get()?.getService()?.applicationContext
|
||||
context?.let {
|
||||
v2rayPoint.packageName = Utils.packagePath(context)
|
||||
v2rayPoint.packageCodePath = context.applicationInfo.nativeLibraryDir + "/"
|
||||
Seq.setContext(context)
|
||||
}
|
||||
Seq.setContext(value?.get()?.getService()?.applicationContext)
|
||||
Libv2ray.initV2Env(Utils.userAssetPath(value?.get()?.getService()))
|
||||
}
|
||||
var currentConfigName = "NG"
|
||||
var currentConfig: ServerConfig? = null
|
||||
|
||||
private var lastQueryTime = 0L
|
||||
private var mBuilder: NotificationCompat.Builder? = null
|
||||
@@ -60,12 +60,12 @@ object V2RayServiceManager {
|
||||
private var mNotificationManager: NotificationManager? = null
|
||||
|
||||
fun startV2Ray(context: Context) {
|
||||
if (context.v2RayApplication.defaultDPreference.getPrefBoolean(SettingsActivity.PREF_PROXY_SHARING, false)) {
|
||||
if (settingsStorage?.decodeBool(AppConfig.PREF_PROXY_SHARING) == true) {
|
||||
context.toast(R.string.toast_warning_pref_proxysharing_short)
|
||||
}else{
|
||||
} else {
|
||||
context.toast(R.string.toast_services_start)
|
||||
}
|
||||
val intent = if (context.v2RayApplication.defaultDPreference.getPrefString(AppConfig.PREF_MODE, "VPN") == "VPN") {
|
||||
val intent = if (settingsStorage?.decodeString(AppConfig.PREF_MODE) ?: "VPN" == "VPN") {
|
||||
Intent(context.applicationContext, V2RayVpnService::class.java)
|
||||
} else {
|
||||
Intent(context.applicationContext, V2RayProxyOnlyService::class.java)
|
||||
@@ -81,12 +81,11 @@ object V2RayServiceManager {
|
||||
override fun shutdown(): Long {
|
||||
val serviceControl = serviceControl?.get() ?: return -1
|
||||
// called by go
|
||||
// shutdown the whole vpn service
|
||||
return try {
|
||||
serviceControl.stopService()
|
||||
0
|
||||
} catch (e: Exception) {
|
||||
Log.d(serviceControl.getService().packageName, e.toString())
|
||||
Log.d(ANG_PACKAGE, e.toString())
|
||||
-1
|
||||
}
|
||||
}
|
||||
@@ -95,9 +94,9 @@ object V2RayServiceManager {
|
||||
return 0
|
||||
}
|
||||
|
||||
override fun protect(l: Long): Long {
|
||||
val serviceControl = serviceControl?.get() ?: return 0
|
||||
return if (serviceControl.vpnProtect(l.toInt())) 0 else 1
|
||||
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 {
|
||||
@@ -109,21 +108,25 @@ object V2RayServiceManager {
|
||||
val serviceControl = serviceControl?.get() ?: return -1
|
||||
//Logger.d(s)
|
||||
return try {
|
||||
serviceControl.startService(s)
|
||||
serviceControl.startService()
|
||||
lastQueryTime = System.currentTimeMillis()
|
||||
startSpeedNotification()
|
||||
0
|
||||
} catch (e: Exception) {
|
||||
Log.d(serviceControl.getService().packageName, e.toString())
|
||||
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)
|
||||
@@ -132,20 +135,17 @@ object V2RayServiceManager {
|
||||
mFilter.addAction(Intent.ACTION_USER_PRESENT)
|
||||
service.registerReceiver(mMsgReceive, mFilter)
|
||||
} catch (e: Exception) {
|
||||
Log.d(service.packageName, e.toString())
|
||||
Log.d(ANG_PACKAGE, e.toString())
|
||||
}
|
||||
|
||||
v2rayPoint.configureFileContent = service.defaultDPreference.getPrefString(AppConfig.PREF_CURR_CONFIG, "")
|
||||
v2rayPoint.enableLocalDNS = service.defaultDPreference.getPrefBoolean(SettingsActivity.PREF_LOCAL_DNS_ENABLED, false)
|
||||
v2rayPoint.forwardIpv6 = service.defaultDPreference.getPrefBoolean(SettingsActivity.PREF_FORWARD_IPV6, false)
|
||||
v2rayPoint.domainName = service.defaultDPreference.getPrefString(AppConfig.PREF_CURR_CONFIG_DOMAIN, "")
|
||||
v2rayPoint.proxyOnly = service.defaultDPreference.getPrefString(AppConfig.PREF_MODE, "VPN") != "VPN"
|
||||
currentConfigName = service.defaultDPreference.getPrefString(AppConfig.PREF_CURR_CONFIG_NAME, "NG")
|
||||
v2rayPoint.configureFileContent = result.content
|
||||
v2rayPoint.domainName = config.getV2rayPointDomainAndPort()
|
||||
currentConfig = config
|
||||
|
||||
try {
|
||||
v2rayPoint.runLoop()
|
||||
v2rayPoint.runLoop(settingsStorage?.decodeBool(AppConfig.PREF_PREFER_IPV6) ?: false)
|
||||
} catch (e: Exception) {
|
||||
Log.d(service.packageName, e.toString())
|
||||
Log.d(ANG_PACKAGE, e.toString())
|
||||
}
|
||||
|
||||
if (v2rayPoint.isRunning) {
|
||||
@@ -162,10 +162,12 @@ object V2RayServiceManager {
|
||||
val service = serviceControl?.get()?.getService() ?: return
|
||||
|
||||
if (v2rayPoint.isRunning) {
|
||||
try {
|
||||
v2rayPoint.stopLoop()
|
||||
} catch (e: Exception) {
|
||||
Log.d(service.packageName, e.toString())
|
||||
GlobalScope.launch(Dispatchers.Default) {
|
||||
try {
|
||||
v2rayPoint.stopLoop()
|
||||
} catch (e: Exception) {
|
||||
Log.d(ANG_PACKAGE, e.toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -175,7 +177,7 @@ object V2RayServiceManager {
|
||||
try {
|
||||
service.unregisterReceiver(mMsgReceive)
|
||||
} catch (e: Exception) {
|
||||
Log.d(service.packageName, e.toString())
|
||||
Log.d(ANG_PACKAGE, e.toString())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -203,35 +205,69 @@ object V2RayServiceManager {
|
||||
AppConfig.MSG_STATE_RESTART -> {
|
||||
startV2rayPoint()
|
||||
}
|
||||
AppConfig.MSG_MEASURE_DELAY -> {
|
||||
measureV2rayDelay()
|
||||
}
|
||||
}
|
||||
|
||||
when (intent?.action) {
|
||||
Intent.ACTION_SCREEN_OFF -> {
|
||||
Log.d(AppConfig.ANG_PACKAGE, "SCREEN_OFF, stop querying stats")
|
||||
Log.d(ANG_PACKAGE, "SCREEN_OFF, stop querying stats")
|
||||
stopSpeedNotification()
|
||||
}
|
||||
Intent.ACTION_SCREEN_ON -> {
|
||||
Log.d(AppConfig.ANG_PACKAGE, "SCREEN_ON, start querying stats")
|
||||
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,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT)
|
||||
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` = AppConfig.ANG_PACKAGE
|
||||
stopV2RayIntent.`package` = ANG_PACKAGE
|
||||
stopV2RayIntent.putExtra("key", AppConfig.MSG_STATE_STOP)
|
||||
|
||||
val stopV2RayPendingIntent = PendingIntent.getBroadcast(service,
|
||||
NOTIFICATION_PENDING_INTENT_STOP_V2RAY, stopV2RayIntent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT)
|
||||
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) {
|
||||
@@ -243,8 +279,8 @@ object V2RayServiceManager {
|
||||
}
|
||||
|
||||
mBuilder = NotificationCompat.Builder(service, channelId)
|
||||
.setSmallIcon(R.drawable.ic_v)
|
||||
.setContentTitle(currentConfigName)
|
||||
.setSmallIcon(R.drawable.ic_stat_name)
|
||||
.setContentTitle(currentConfig?.remarks)
|
||||
.setPriority(NotificationCompat.PRIORITY_MIN)
|
||||
.setOngoing(true)
|
||||
.setShowWhen(false)
|
||||
@@ -281,10 +317,10 @@ object V2RayServiceManager {
|
||||
mSubscription = null
|
||||
}
|
||||
|
||||
private fun updateNotification(contentText: String, proxyTraffic: Long, directTraffic: Long) {
|
||||
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_v)
|
||||
mBuilder?.setSmallIcon(R.drawable.ic_stat_name)
|
||||
} else if (proxyTraffic > directTraffic) {
|
||||
mBuilder?.setSmallIcon(R.drawable.ic_stat_proxy)
|
||||
} else {
|
||||
@@ -304,14 +340,13 @@ object V2RayServiceManager {
|
||||
return mNotificationManager
|
||||
}
|
||||
|
||||
fun startSpeedNotification() {
|
||||
val service = serviceControl?.get()?.getService() ?: return
|
||||
private fun startSpeedNotification() {
|
||||
if (mSubscription == null &&
|
||||
v2rayPoint.isRunning &&
|
||||
service.defaultDPreference.getPrefBoolean(SettingsActivity.PREF_SPEED_ENABLED, false)) {
|
||||
settingsStorage?.decodeBool(AppConfig.PREF_SPEED_ENABLED) == true) {
|
||||
var lastZeroSpeed = false
|
||||
val outboundTags = service.defaultDPreference.getPrefStringOrderedSet(AppConfig.PREF_CURR_CONFIG_OUTBOUND_TAGS, LinkedHashSet())
|
||||
outboundTags.remove(TAG_DIRECT)
|
||||
val outboundTags = currentConfig?.getAllOutboundTags()
|
||||
outboundTags?.remove(TAG_DIRECT)
|
||||
|
||||
mSubscription = Observable.interval(3, java.util.concurrent.TimeUnit.SECONDS)
|
||||
.subscribe {
|
||||
@@ -319,7 +354,7 @@ object V2RayServiceManager {
|
||||
val sinceLastQueryInSeconds = (queryTime - lastQueryTime) / 1000.0
|
||||
var proxyTotal = 0L
|
||||
val text = StringBuilder()
|
||||
outboundTags.forEach {
|
||||
outboundTags?.forEach {
|
||||
val up = v2rayPoint.queryStats(it, "uplink")
|
||||
val down = v2rayPoint.queryStats(it, "downlink")
|
||||
if (up + down > 0) {
|
||||
@@ -332,7 +367,7 @@ object V2RayServiceManager {
|
||||
val zeroSpeed = (proxyTotal == 0L && directUplink == 0L && directDownlink == 0L)
|
||||
if (!zeroSpeed || !lastZeroSpeed) {
|
||||
if (proxyTotal == 0L) {
|
||||
appendSpeedString(text, outboundTags.firstOrNull(), 0.0, 0.0)
|
||||
appendSpeedString(text, outboundTags?.firstOrNull(), 0.0, 0.0)
|
||||
}
|
||||
appendSpeedString(text, TAG_DIRECT, directUplink / sinceLastQueryInSeconds,
|
||||
directDownlink / sinceLastQueryInSeconds)
|
||||
@@ -354,11 +389,11 @@ object V2RayServiceManager {
|
||||
text.append("• ${up.toLong().toSpeedString()}↑ ${down.toLong().toSpeedString()}↓\n")
|
||||
}
|
||||
|
||||
fun stopSpeedNotification() {
|
||||
private fun stopSpeedNotification() {
|
||||
if (mSubscription != null) {
|
||||
mSubscription?.unsubscribe() //stop queryStats
|
||||
mSubscription = null
|
||||
updateNotification(currentConfigName, 0, 0)
|
||||
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,6 +1,6 @@
|
||||
package com.v2ray.ang.service
|
||||
|
||||
import android.app.*
|
||||
import android.app.Service
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
@@ -8,12 +8,14 @@ import android.net.*
|
||||
import android.os.Build
|
||||
import android.os.ParcelFileDescriptor
|
||||
import android.os.StrictMode
|
||||
import android.support.annotation.RequiresApi
|
||||
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.ui.PerAppProxyActivity
|
||||
import com.v2ray.ang.ui.SettingsActivity
|
||||
import com.v2ray.ang.dto.ERoutingMode
|
||||
import com.v2ray.ang.util.MmkvManager
|
||||
import com.v2ray.ang.util.MyContextWrapper
|
||||
import com.v2ray.ang.util.Utils
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
@@ -22,17 +24,31 @@ import java.io.File
|
||||
import java.lang.ref.SoftReference
|
||||
|
||||
class V2RayVpnService : VpnService(), ServiceControl {
|
||||
companion object {
|
||||
private const val VPN_MTU = 1500
|
||||
private const val PRIVATE_VLAN4_CLIENT = "26.26.26.1"
|
||||
private const val PRIVATE_VLAN4_ROUTER = "26.26.26.2"
|
||||
private const val PRIVATE_VLAN6_CLIENT = "da26:2626::1"
|
||||
private const val PRIVATE_VLAN6_ROUTER = "da26:2626::2"
|
||||
private const val TUN2SOCKS = "libtun2socks.so"
|
||||
}
|
||||
|
||||
private val settingsStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_SETTING, MMKV.MULTI_PROCESS_MODE) }
|
||||
|
||||
private lateinit var mInterface: ParcelFileDescriptor
|
||||
|
||||
/**
|
||||
* Unfortunately registerDefaultNetworkCallback is going to return our VPN interface: https://android.googlesource.com/platform/frameworks/base/+/dda156ab0c5d66ad82bdcf76cda07cbc0a9c8a2e
|
||||
*
|
||||
* This makes doing a requestNetwork with REQUEST necessary so that we don't get ALL possible networks that
|
||||
* satisfies default network capabilities but only THE default network. Unfortunately we need to have
|
||||
* android.permission.CHANGE_NETWORK_STATE to be able to call requestNetwork.
|
||||
*
|
||||
* Source: https://android.googlesource.com/platform/frameworks/base/+/2df4c7d/services/core/java/com/android/server/ConnectivityService.java#887
|
||||
*/
|
||||
//val fd: Int get() = mInterface.fd
|
||||
private lateinit var process: Process
|
||||
|
||||
/**destroy
|
||||
* Unfortunately registerDefaultNetworkCallback is going to return our VPN interface: https://android.googlesource.com/platform/frameworks/base/+/dda156ab0c5d66ad82bdcf76cda07cbc0a9c8a2e
|
||||
*
|
||||
* This makes doing a requestNetwork with REQUEST necessary so that we don't get ALL possible networks that
|
||||
* satisfies default network capabilities but only THE default network. Unfortunately we need to have
|
||||
* android.permission.CHANGE_NETWORK_STATE to be able to call requestNetwork.
|
||||
*
|
||||
* Source: https://android.googlesource.com/platform/frameworks/base/+/2df4c7d/services/core/java/com/android/server/ConnectivityService.java#887
|
||||
*/
|
||||
@delegate:RequiresApi(Build.VERSION_CODES.P)
|
||||
private val defaultNetworkRequest by lazy {
|
||||
NetworkRequest.Builder()
|
||||
@@ -49,10 +65,12 @@ class V2RayVpnService : VpnService(), ServiceControl {
|
||||
override fun onAvailable(network: Network) {
|
||||
setUnderlyingNetworks(arrayOf(network))
|
||||
}
|
||||
|
||||
override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) {
|
||||
// it's a good idea to refresh capabilities
|
||||
setUnderlyingNetworks(arrayOf(network))
|
||||
}
|
||||
|
||||
override fun onLost(network: Network) {
|
||||
setUnderlyingNetworks(null)
|
||||
}
|
||||
@@ -71,18 +89,17 @@ class V2RayVpnService : VpnService(), ServiceControl {
|
||||
stopV2Ray()
|
||||
}
|
||||
|
||||
override fun onLowMemory() {
|
||||
stopV2Ray()
|
||||
super.onLowMemory()
|
||||
}
|
||||
// override fun onLowMemory() {
|
||||
// stopV2Ray()
|
||||
// super.onLowMemory()
|
||||
// }
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
stopV2Ray()
|
||||
V2RayServiceManager.cancelNotification()
|
||||
}
|
||||
|
||||
private fun setup(parameters: String) {
|
||||
|
||||
private fun setup() {
|
||||
val prepare = prepare(this)
|
||||
if (prepare != null) {
|
||||
return
|
||||
@@ -91,47 +108,47 @@ class V2RayVpnService : VpnService(), ServiceControl {
|
||||
// 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 routingMode = defaultDPreference.getPrefString(SettingsActivity.PREF_ROUTING_MODE, "0")
|
||||
//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' -> {
|
||||
if (routingMode == "1" || routingMode == "3") {
|
||||
if (it[1] == "::") { //not very elegant, should move Vpn setting in Kotlin, simplify go code
|
||||
builder.addRoute("2000::", 3)
|
||||
} else {
|
||||
resources.getStringArray(R.array.bypass_private_ip_address).forEach { cidr ->
|
||||
val addr = cidr.split('/')
|
||||
builder.addRoute(addr[0], addr[1].toInt())
|
||||
}
|
||||
}
|
||||
} else {
|
||||
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
|
||||
|
||||
if(!enableLocalDns) {
|
||||
Utils.getRemoteDnsServers(defaultDPreference)
|
||||
.forEach {
|
||||
builder.addDnsServer(it)
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
builder.setSession(V2RayServiceManager.currentConfigName)
|
||||
if (settingsStorage?.decodeBool(AppConfig.PREF_PREFER_IPV6) == true) {
|
||||
builder.addAddress(PRIVATE_VLAN6_CLIENT, 126)
|
||||
if (routingMode == ERoutingMode.BYPASS_LAN.value || routingMode == ERoutingMode.BYPASS_LAN_MAINLAND.value) {
|
||||
builder.addRoute("2000::", 3) //currently only 1/8 of total ipV6 is in use
|
||||
} else {
|
||||
builder.addRoute("::", 0)
|
||||
}
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP &&
|
||||
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_LOCAL_DNS_ENABLED) == true) {
|
||||
builder.addDnsServer(PRIVATE_VLAN4_ROUTER)
|
||||
} else {
|
||||
Utils.getVpnDnsServers()
|
||||
.forEach {
|
||||
if (Utils.isPureIpAddress(it)) {
|
||||
builder.addDnsServer(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
builder.setSession(V2RayServiceManager.currentConfig?.remarks.orEmpty())
|
||||
|
||||
if (settingsStorage?.decodeBool(AppConfig.PREF_PER_APP_PROXY) == true) {
|
||||
val apps = settingsStorage?.decodeStringSet(AppConfig.PREF_PER_APP_PROXY_SET)
|
||||
val bypassApps = settingsStorage?.decodeBool(AppConfig.PREF_BYPASS_APPS) ?: false
|
||||
apps?.forEach {
|
||||
try {
|
||||
if (bypassApps)
|
||||
@@ -166,23 +183,59 @@ class V2RayVpnService : VpnService(), ServiceControl {
|
||||
// Create a new interface using the builder and save the parameters.
|
||||
try {
|
||||
mInterface = builder.establish()!!
|
||||
runTun2socks()
|
||||
} catch (e: Exception) {
|
||||
// non-nullable lateinit var
|
||||
e.printStackTrace()
|
||||
stopV2Ray()
|
||||
}
|
||||
}
|
||||
|
||||
sendFd()
|
||||
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()
|
||||
} catch (e: Exception) {
|
||||
Log.d(packageName, e.toString())
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
var tries = 0
|
||||
while (true) try {
|
||||
Thread.sleep(1000L shl tries)
|
||||
Thread.sleep(50L shl tries)
|
||||
Log.d(packageName, "sendFd tries: $tries")
|
||||
LocalSocket().use { localSocket ->
|
||||
localSocket.connect(LocalSocketAddress(path, LocalSocketAddress.Namespace.FILESYSTEM))
|
||||
@@ -217,6 +270,13 @@ class V2RayVpnService : VpnService(), ServiceControl {
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
Log.d(packageName, "tun2socks destroy")
|
||||
process.destroy()
|
||||
} catch (e: Exception) {
|
||||
Log.d(packageName, e.toString())
|
||||
}
|
||||
|
||||
V2RayServiceManager.stopV2rayPoint()
|
||||
|
||||
if (isForced) {
|
||||
@@ -232,7 +292,6 @@ class V2RayVpnService : VpnService(), ServiceControl {
|
||||
} catch (ignored: Exception) {
|
||||
// ignored
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -240,8 +299,8 @@ class V2RayVpnService : VpnService(), ServiceControl {
|
||||
return this
|
||||
}
|
||||
|
||||
override fun startService(parameters: String) {
|
||||
setup(parameters)
|
||||
override fun startService() {
|
||||
setup()
|
||||
}
|
||||
|
||||
override fun stopService() {
|
||||
@@ -252,4 +311,11 @@ class V2RayVpnService : VpnService(), ServiceControl {
|
||||
return protect(socket)
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.N)
|
||||
override fun attachBaseContext(newBase: Context?) {
|
||||
val context = newBase?.let {
|
||||
MyContextWrapper.wrap(newBase, Utils.getLocale(newBase))
|
||||
}
|
||||
super.attachBaseContext(context)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,199 +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.R
|
||||
|
||||
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.sub_setting -> activityClass = SubSettingActivity::class.java
|
||||
R.id.settings -> activityClass = SettingsActivity::class.java
|
||||
R.id.logcat -> {
|
||||
startActivity(Intent(this@BaseDrawerActivity, LogcatActivity::class.java))
|
||||
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 (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,27 +1,33 @@
|
||||
package com.v2ray.ang.ui
|
||||
|
||||
import android.os.Bundle
|
||||
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 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)
|
||||
|
||||
@@ -32,9 +38,9 @@ class LogcatActivity : BaseActivity() {
|
||||
private fun logcat(shouldFlushLog: Boolean) {
|
||||
|
||||
try {
|
||||
pb_waiting.visibility = View.VISIBLE
|
||||
binding.pbWaiting.visibility = View.VISIBLE
|
||||
|
||||
GlobalScope.launch(Dispatchers.Default) {
|
||||
lifecycleScope.launch(Dispatchers.Default) {
|
||||
if (shouldFlushLog) {
|
||||
val lst = LinkedHashSet<String>()
|
||||
lst.add("logcat")
|
||||
@@ -48,17 +54,17 @@ class LogcatActivity : BaseActivity() {
|
||||
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() }
|
||||
launch(Dispatchers.Main) {
|
||||
tv_logcat.text = allText
|
||||
tv_logcat.movementMethod = ScrollingMovementMethod()
|
||||
pb_waiting.visibility = View.GONE
|
||||
Handler(Looper.getMainLooper()).post { sv_logcat.fullScroll(View.FOCUS_DOWN) }
|
||||
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) {
|
||||
@@ -66,18 +72,18 @@ class LogcatActivity : BaseActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
|
||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||
menuInflater.inflate(R.menu.menu_logcat, menu)
|
||||
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.delete -> {
|
||||
R.id.clear_all -> {
|
||||
logcat(true)
|
||||
true
|
||||
}
|
||||
|
||||
@@ -1,182 +1,216 @@
|
||||
package com.v2ray.ang.ui
|
||||
|
||||
import android.Manifest
|
||||
import android.arch.lifecycle.ViewModelProviders
|
||||
import android.content.Intent
|
||||
import android.content.*
|
||||
import android.net.Uri
|
||||
import android.net.VpnService
|
||||
import android.os.Bundle
|
||||
import android.support.design.widget.NavigationView
|
||||
import android.support.v4.view.GravityCompat
|
||||
import android.support.v7.app.ActionBarDrawerToggle
|
||||
import android.support.v7.widget.LinearLayoutManager
|
||||
import android.support.v7.widget.helper.ItemTouchHelper
|
||||
import android.text.TextUtils
|
||||
import android.util.Log
|
||||
import android.view.KeyEvent
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import com.tbruyelle.rxpermissions.RxPermissions
|
||||
import com.v2ray.ang.AppConfig
|
||||
import com.v2ray.ang.BuildConfig
|
||||
import com.v2ray.ang.R
|
||||
import com.v2ray.ang.extension.defaultDPreference
|
||||
import android.os.Bundle
|
||||
import android.text.TextUtils
|
||||
import android.view.KeyEvent
|
||||
import com.v2ray.ang.AppConfig
|
||||
import android.content.res.ColorStateList
|
||||
import com.google.android.material.navigation.NavigationView
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.GravityCompat
|
||||
import androidx.appcompat.app.ActionBarDrawerToggle
|
||||
import androidx.recyclerview.widget.ItemTouchHelper
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.tencent.mmkv.MMKV
|
||||
import com.v2ray.ang.AppConfig.ANG_PACKAGE
|
||||
import com.v2ray.ang.BuildConfig
|
||||
import com.v2ray.ang.databinding.ActivityMainBinding
|
||||
import com.v2ray.ang.dto.EConfigType
|
||||
import com.v2ray.ang.extension.toast
|
||||
import com.v2ray.ang.helper.SimpleItemTouchHelperCallback
|
||||
import com.v2ray.ang.util.AngConfigManager
|
||||
import com.v2ray.ang.util.Utils
|
||||
import com.v2ray.ang.util.V2rayConfigUtil
|
||||
import com.v2ray.ang.viewmodel.MainViewModel
|
||||
import kotlinx.android.synthetic.main.activity_main.*
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import libv2ray.Libv2ray
|
||||
import rx.Observable
|
||||
import rx.android.schedulers.AndroidSchedulers
|
||||
import java.net.URL
|
||||
import java.util.concurrent.TimeUnit
|
||||
import com.v2ray.ang.helper.SimpleItemTouchHelperCallback
|
||||
import com.v2ray.ang.service.V2RayServiceManager
|
||||
import com.v2ray.ang.util.*
|
||||
import com.v2ray.ang.viewmodel.MainViewModel
|
||||
import kotlinx.coroutines.*
|
||||
import me.drakeet.support.toast.ToastCompat
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
|
||||
class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedListener {
|
||||
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
|
||||
}
|
||||
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
|
||||
private val mainViewModel: MainViewModel by lazy { ViewModelProviders.of(this).get(MainViewModel::class.java) }
|
||||
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 {
|
||||
binding.fab.setOnClickListener {
|
||||
if (mainViewModel.isRunning.value == true) {
|
||||
Utils.stopVService(this)
|
||||
} else if (defaultDPreference.getPrefString(AppConfig.PREF_MODE, "VPN") == "VPN") {
|
||||
} 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)
|
||||
requestVpnPermission.launch(intent)
|
||||
}
|
||||
} else {
|
||||
startV2Ray()
|
||||
}
|
||||
}
|
||||
layout_test.setOnClickListener {
|
||||
binding.layoutTest.setOnClickListener {
|
||||
if (mainViewModel.isRunning.value == true) {
|
||||
tv_test_state.text = getString(R.string.connection_test_testing)
|
||||
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)
|
||||
version.text = "v${BuildConfig.VERSION_NAME} (${Libv2ray.checkVersionX()})"
|
||||
binding.navView.setNavigationItemSelectedListener(this)
|
||||
binding.version.text = "v${BuildConfig.VERSION_NAME} (${SpeedtestUtil.getLibVersion()})"
|
||||
|
||||
setupViewModelObserver()
|
||||
setupViewModel()
|
||||
copyAssets()
|
||||
migrateLegacy()
|
||||
}
|
||||
|
||||
private fun setupViewModelObserver() {
|
||||
mainViewModel.updateListAction.observe(this, {
|
||||
val index = it ?: return@observe
|
||||
private fun setupViewModel() {
|
||||
mainViewModel.updateListAction.observe(this) { index ->
|
||||
if (index >= 0) {
|
||||
adapter.updateSelectedItem(index)
|
||||
adapter.notifyItemChanged(index)
|
||||
} else {
|
||||
adapter.updateConfigList()
|
||||
adapter.notifyDataSetChanged()
|
||||
}
|
||||
})
|
||||
mainViewModel.updateTestResultAction.observe(this, { tv_test_state.text = it })
|
||||
mainViewModel.isRunning.observe(this, {
|
||||
val isRunning = it ?: return@observe
|
||||
adapter.changeable = !isRunning
|
||||
}
|
||||
mainViewModel.updateTestResultAction.observe(this) { setTestState(it) }
|
||||
mainViewModel.isRunning.observe(this) { isRunning ->
|
||||
adapter.isRunning = isRunning
|
||||
if (isRunning) {
|
||||
fab.setImageResource(R.drawable.ic_v)
|
||||
tv_test_state.text = getString(R.string.connection_connected)
|
||||
binding.fab.backgroundTintList = ColorStateList.valueOf(ContextCompat.getColor(this, R.color.colorSelected))
|
||||
setTestState(getString(R.string.connection_connected))
|
||||
binding.layoutTest.isFocusable = true
|
||||
} else {
|
||||
fab.setImageResource(R.drawable.ic_v_idle)
|
||||
tv_test_state.text = getString(R.string.connection_not_connected)
|
||||
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, AngConfigManager.configs.index)) {
|
||||
hideCircle()
|
||||
V2RayServiceManager.startV2Ray(this)
|
||||
hideCircle()
|
||||
}
|
||||
|
||||
fun restartV2Ray() {
|
||||
if (mainViewModel.isRunning.value == true) {
|
||||
Utils.stopVService(this)
|
||||
}
|
||||
Observable.timer(500, TimeUnit.MILLISECONDS)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe {
|
||||
startV2Ray()
|
||||
}
|
||||
}
|
||||
|
||||
public override fun onResume() {
|
||||
super.onResume()
|
||||
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 -> {
|
||||
val uri = data?.data
|
||||
if (resultCode == RESULT_OK && uri != null) {
|
||||
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
|
||||
}
|
||||
|
||||
private fun getOptionIntent() = Intent().putExtra("position", -1)
|
||||
.putExtra("isRunning", mainViewModel.isRunning.value == true)
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
|
||||
R.id.import_qrcode -> {
|
||||
importQRcode(REQUEST_SCAN)
|
||||
importQRcode(true)
|
||||
true
|
||||
}
|
||||
R.id.import_clipboard -> {
|
||||
@@ -184,18 +218,23 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
||||
true
|
||||
}
|
||||
R.id.import_manually_vmess -> {
|
||||
startActivity(getOptionIntent().setClass(this, ServerActivity::class.java))
|
||||
adapter.updateConfigList()
|
||||
importManually(EConfigType.VMESS.value)
|
||||
true
|
||||
}
|
||||
R.id.import_manually_vless -> {
|
||||
importManually(EConfigType.VLESS.value)
|
||||
true
|
||||
}
|
||||
R.id.import_manually_ss -> {
|
||||
startActivity(getOptionIntent().setClass(this, Server3Activity::class.java))
|
||||
adapter.updateConfigList()
|
||||
importManually(EConfigType.SHADOWSOCKS.value)
|
||||
true
|
||||
}
|
||||
R.id.import_manually_socks -> {
|
||||
startActivity(getOptionIntent().setClass(this, Server4Activity::class.java))
|
||||
adapter.updateConfigList()
|
||||
importManually(EConfigType.SOCKS.value)
|
||||
true
|
||||
}
|
||||
R.id.import_manually_trojan -> {
|
||||
importManually(EConfigType.TROJAN.value)
|
||||
true
|
||||
}
|
||||
R.id.import_config_custom_clipboard -> {
|
||||
@@ -211,7 +250,7 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
||||
true
|
||||
}
|
||||
R.id.import_config_custom_url_scan -> {
|
||||
importQRcode(REQUEST_SCAN_URL)
|
||||
importQRcode(false)
|
||||
true
|
||||
}
|
||||
|
||||
@@ -226,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)
|
||||
}
|
||||
@@ -239,22 +278,61 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
||||
true
|
||||
}
|
||||
|
||||
// R.id.settings -> {
|
||||
// startActivity<SettingsActivity>("isRunning" to isRunning)
|
||||
// true
|
||||
// }
|
||||
// R.id.logcat -> {
|
||||
// startActivity<LogcatActivity>()
|
||||
// true
|
||||
// }
|
||||
R.id.real_ping_all -> {
|
||||
mainViewModel.testAllRealPing()
|
||||
true
|
||||
}
|
||||
|
||||
R.id.service_restart -> {
|
||||
restartV2Ray()
|
||||
true
|
||||
}
|
||||
|
||||
R.id.del_all_config -> {
|
||||
AlertDialog.Builder(this).setMessage(R.string.del_config_comfirm)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
MmkvManager.removeAllServer()
|
||||
mainViewModel.reloadServerList()
|
||||
}
|
||||
.show()
|
||||
true
|
||||
}
|
||||
|
||||
R.id.del_invalid_config -> {
|
||||
AlertDialog.Builder(this).setMessage(R.string.del_config_comfirm)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
MmkvManager.removeInvalidServer()
|
||||
mainViewModel.reloadServerList()
|
||||
}
|
||||
.show()
|
||||
true
|
||||
}
|
||||
R.id.sort_by_test_results -> {
|
||||
MmkvManager.sortByTestResults()
|
||||
mainViewModel.reloadServerList()
|
||||
true
|
||||
}
|
||||
R.id.filter_config -> {
|
||||
mainViewModel.filterConfig(this)
|
||||
true
|
||||
}
|
||||
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
private fun importManually(createConfigType : Int) {
|
||||
startActivity(
|
||||
Intent()
|
||||
.putExtra("createConfigType", createConfigType)
|
||||
.putExtra("subscriptionId", mainViewModel.subscriptionId)
|
||||
.setClass(this, ServerActivity::class.java)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* import config from qrcode
|
||||
*/
|
||||
fun importQRcode(requestCode: Int): Boolean {
|
||||
fun importQRcode(forConfig: Boolean): Boolean {
|
||||
// try {
|
||||
// startActivityForResult(Intent("com.google.zxing.client.android.SCAN")
|
||||
// .addCategory(Intent.CATEGORY_DEFAULT)
|
||||
@@ -264,7 +342,10 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
||||
.request(Manifest.permission.CAMERA)
|
||||
.subscribe {
|
||||
if (it)
|
||||
startActivityForResult(Intent(this, ScannerActivity::class.java), 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)
|
||||
}
|
||||
@@ -272,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
|
||||
*/
|
||||
@@ -288,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)
|
||||
}
|
||||
@@ -350,9 +453,9 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
||||
toast(R.string.toast_invalid_url)
|
||||
return false
|
||||
}
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
val configText = try {
|
||||
URL(url).readText()
|
||||
Utils.getUrlContentWithCustomUserAgent(url)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
""
|
||||
@@ -375,29 +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)
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
Log.d(ANG_PACKAGE, url)
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
val configText = try {
|
||||
URL(url).readText()
|
||||
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(Utils.decode(configText), id)
|
||||
importBatchConfig(configText, it.first)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -417,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
|
||||
*/
|
||||
@@ -434,9 +546,8 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
||||
.subscribe {
|
||||
if (it) {
|
||||
try {
|
||||
contentResolver.openInputStream(uri).use {
|
||||
val configText = it?.bufferedReader()?.readText()
|
||||
importCustomizeConfig(configText)
|
||||
contentResolver.openInputStream(uri).use { input ->
|
||||
importCustomizeConfig(input?.bufferedReader()?.readText())
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
@@ -450,22 +561,26 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
||||
* import customize config
|
||||
*/
|
||||
fun importCustomizeConfig(server: String?) {
|
||||
if (server == null) {
|
||||
return
|
||||
}
|
||||
if (!V2rayConfigUtil.isValidConfig(server)) {
|
||||
toast(R.string.toast_config_file_invalid)
|
||||
return
|
||||
}
|
||||
val resId = AngConfigManager.importCustomizeConfig(server)
|
||||
if (resId > 0) {
|
||||
toast(resId)
|
||||
} else {
|
||||
try {
|
||||
if (server == null || TextUtils.isEmpty(server)) {
|
||||
toast(R.string.toast_none_data)
|
||||
return
|
||||
}
|
||||
mainViewModel.appendCustomConfigServer(server)
|
||||
mainViewModel.reloadServerList()
|
||||
toast(R.string.toast_success)
|
||||
adapter.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?) {
|
||||
// }
|
||||
@@ -484,7 +599,7 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
||||
}
|
||||
|
||||
fun showCircle() {
|
||||
fabProgressCircle?.show()
|
||||
binding.fabProgressCircle.show()
|
||||
}
|
||||
|
||||
fun hideCircle() {
|
||||
@@ -492,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()
|
||||
}
|
||||
@@ -519,20 +639,20 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
||||
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(Intent(this, LogcatActivity::class.java))
|
||||
}
|
||||
}
|
||||
drawer_layout.closeDrawer(GravityCompat.START)
|
||||
binding.drawerLayout.closeDrawer(GravityCompat.START)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,24 +2,29 @@ package com.v2ray.ang.ui
|
||||
|
||||
import android.content.Intent
|
||||
import android.graphics.Color
|
||||
import android.support.v7.app.AlertDialog
|
||||
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 com.v2ray.ang.util.V2rayConfigUtil
|
||||
import kotlinx.android.synthetic.main.item_qrcode.view.*
|
||||
import kotlinx.android.synthetic.main.item_recycler_main.view.*
|
||||
import rx.Observable
|
||||
import rx.android.schedulers.AndroidSchedulers
|
||||
import java.util.concurrent.TimeUnit
|
||||
@@ -32,81 +37,88 @@ 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 = EConfigType.fromInt(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)
|
||||
val outbound = config.getProxyOutbound()
|
||||
val aff = MmkvManager.decodeServerAffiliationInfo(guid)
|
||||
|
||||
holder.itemMainBinding.tvName.text = config.remarks
|
||||
holder.itemView.setBackgroundColor(Color.TRANSPARENT)
|
||||
holder.test_result.text = test_result
|
||||
|
||||
if (TextUtils.isEmpty(subid)) {
|
||||
holder.subid.text = ""
|
||||
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
|
||||
}
|
||||
|
||||
var shareOptions = share_method.asList()
|
||||
if (configType == EConfigType.CUSTOM) {
|
||||
holder.type.text = mActivity.getString(R.string.server_customize_config)
|
||||
val serverOutbound = V2rayConfigUtil.getCustomConfigServerOutbound(mActivity.applicationContext, configs.vmess[position].guid)
|
||||
if (serverOutbound == null) {
|
||||
holder.statistics.text = ""
|
||||
} else {
|
||||
holder.statistics.text = "${serverOutbound.getServerAddress()} : ${serverOutbound.getServerPort()}"
|
||||
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()
|
||||
}
|
||||
shareOptions = shareOptions.takeLast(1)
|
||||
} else {
|
||||
holder.type.text = configType?.name?.toLowerCase()
|
||||
holder.statistics.text = "$address : $port"
|
||||
}
|
||||
holder.itemMainBinding.tvStatistics.text = "${outbound?.getServerAddress()} : ${outbound?.getServerPort()}"
|
||||
|
||||
holder.layout_share.setOnClickListener {
|
||||
holder.itemMainBinding.layoutShare.setOnClickListener {
|
||||
AlertDialog.Builder(mActivity).setItems(shareOptions.toTypedArray()) { _, i ->
|
||||
try {
|
||||
when (i) {
|
||||
0 -> {
|
||||
if (configType == EConfigType.CUSTOM) {
|
||||
shareFullContent(position)
|
||||
if (config.configType == EConfigType.CUSTOM) {
|
||||
shareFullContent(guid)
|
||||
} else {
|
||||
val iv = mActivity.layoutInflater.inflate(R.layout.item_qrcode, null)
|
||||
iv.iv_qcode.setImageBitmap(AngConfigManager.share2QRCode(position))
|
||||
AlertDialog.Builder(mActivity).setView(iv).show()
|
||||
val ivBinding = ItemQrcodeBinding.inflate(LayoutInflater.from(mActivity))
|
||||
ivBinding.ivQcode.setImageBitmap(AngConfigManager.share2QRCode(guid))
|
||||
AlertDialog.Builder(mActivity).setView(ivBinding.root).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 -> shareFullContent(position)
|
||||
2 -> shareFullContent(guid)
|
||||
else -> mActivity.toast("else")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
@@ -115,157 +127,134 @@ class MainRecyclerAdapter(val activity: MainActivity) : RecyclerView.Adapter<Mai
|
||||
}.show()
|
||||
}
|
||||
|
||||
holder.layout_edit.setOnClickListener {
|
||||
val intent = Intent().putExtra("position", position)
|
||||
.putExtra("isRunning", !changeable)
|
||||
if (configType == EConfigType.VMESS) {
|
||||
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))
|
||||
} else if (configType == EConfigType.CUSTOM) {
|
||||
mActivity.startActivity(intent.setClass(mActivity, Server2Activity::class.java))
|
||||
} else if (configType == EConfigType.SHADOWSOCKS) {
|
||||
mActivity.startActivity(intent.setClass(mActivity, Server3Activity::class.java))
|
||||
} else if (configType == EConfigType.SOCKS) {
|
||||
mActivity.startActivity(intent.setClass(mActivity, Server4Activity::class.java))
|
||||
}
|
||||
}
|
||||
holder.layout_remove.setOnClickListener {
|
||||
if (configs.index != position) {
|
||||
if (AngConfigManager.removeServer(position) == 0) {
|
||||
notifyItemRemoved(position)
|
||||
updateSelectedItem(position)
|
||||
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.infoContainer.setOnClickListener {
|
||||
if (changeable) {
|
||||
AngConfigManager.setActiveServer(position)
|
||||
} else {
|
||||
mActivity.showCircle()
|
||||
Utils.stopVService(mActivity)
|
||||
Observable.timer(500, TimeUnit.MILLISECONDS)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe {
|
||||
mActivity.showCircle()
|
||||
if (!Utils.startVService(mActivity, position)) {
|
||||
holder.itemMainBinding.infoContainer.setOnClickListener {
|
||||
val selected = mainStorage?.decodeString(MmkvManager.KEY_SELECTED_SERVER)
|
||||
if (guid != selected) {
|
||||
mainStorage?.encode(MmkvManager.KEY_SELECTED_SERVER, guid)
|
||||
if (!TextUtils.isEmpty(selected)) {
|
||||
notifyItemChanged(mActivity.mainViewModel.getPosition(selected!!))
|
||||
}
|
||||
notifyItemChanged(mActivity.mainViewModel.getPosition(guid))
|
||||
if (isRunning) {
|
||||
mActivity.showCircle()
|
||||
Utils.stopVService(mActivity)
|
||||
Observable.timer(500, TimeUnit.MILLISECONDS)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe {
|
||||
V2RayServiceManager.startV2Ray(mActivity)
|
||||
mActivity.hideCircle()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
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(position: Int) {
|
||||
if (AngConfigManager.shareFullContent2Clipboard(position) == 0) {
|
||||
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(LayoutInflater.from(parent.context)
|
||||
.inflate(R.layout.item_recycler_main, parent, false))
|
||||
MainViewHolder(ItemRecyclerMainBinding.inflate(LayoutInflater.from(parent.context), parent, false))
|
||||
else ->
|
||||
return FooterViewHolder(LayoutInflater.from(parent.context)
|
||||
.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) {
|
||||
notifyItemRemoved(position)
|
||||
}
|
||||
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() {
|
||||
AngConfigManager.storeConfigFile()
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,58 +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.DividerItemDecoration
|
||||
import android.support.v7.widget.LinearLayoutManager
|
||||
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.v2ray.ang.R
|
||||
import com.v2ray.ang.extension.defaultDPreference
|
||||
import com.v2ray.ang.util.AppManagerUtil
|
||||
import kotlinx.android.synthetic.main.activity_bypass_list.*
|
||||
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 androidx.appcompat.widget.SearchView
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.preference.PreferenceManager
|
||||
import androidx.recyclerview.widget.DividerItemDecoration
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.v2ray.ang.AppConfig
|
||||
import com.v2ray.ang.AppConfig.ANG_PACKAGE
|
||||
import com.v2ray.ang.R
|
||||
import com.v2ray.ang.databinding.ActivityBypassListBinding
|
||||
import com.v2ray.ang.dto.AppInfo
|
||||
import com.v2ray.ang.extension.toast
|
||||
import com.v2ray.ang.extension.v2RayApplication
|
||||
import com.v2ray.ang.util.AppManagerUtil
|
||||
import com.v2ray.ang.util.Utils
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import java.net.URL
|
||||
import rx.android.schedulers.AndroidSchedulers
|
||||
import rx.schedulers.Schedulers
|
||||
import java.text.Collator
|
||||
import java.util.*
|
||||
|
||||
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 = DividerItemDecoration(this, LinearLayoutManager.VERTICAL)
|
||||
recycler_view.addItemDecoration(dividerItemDecoration)
|
||||
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())
|
||||
@@ -65,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
|
||||
@@ -92,103 +86,123 @@ 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
|
||||
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
||||
dst += dy
|
||||
if (dst > threshold) {
|
||||
header_view.hide()
|
||||
dst = 0
|
||||
} else if (dst < -20) {
|
||||
header_view.show()
|
||||
dst = 0
|
||||
}
|
||||
}
|
||||
var dst = 0
|
||||
val threshold = resources.getDimensionPixelSize(R.dimen.bypass_list_header_height) * 2
|
||||
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
||||
dst += dy
|
||||
if (dst > threshold) {
|
||||
header_view.hide()
|
||||
dst = 0
|
||||
} else if (dst < -20) {
|
||||
header_view.show()
|
||||
dst = 0
|
||||
}
|
||||
}
|
||||
|
||||
var hiding = false
|
||||
fun View.hide() {
|
||||
val target = -height.toFloat()
|
||||
if (hiding || translationY == target) return
|
||||
animate()
|
||||
.translationY(target)
|
||||
.setInterpolator(AccelerateInterpolator(2F))
|
||||
.setListener(object : AnimatorListenerAdapter() {
|
||||
override fun onAnimationEnd(animation: Animator?) {
|
||||
hiding = false
|
||||
}
|
||||
})
|
||||
hiding = true
|
||||
}
|
||||
|
||||
var showing = false
|
||||
fun View.show() {
|
||||
val target = 0f
|
||||
if (showing || translationY == target) return
|
||||
animate()
|
||||
.translationY(target)
|
||||
.setInterpolator(DecelerateInterpolator(2F))
|
||||
.setListener(object : AnimatorListenerAdapter() {
|
||||
override fun onAnimationEnd(animation: Animator?) {
|
||||
showing = false
|
||||
}
|
||||
})
|
||||
showing = true
|
||||
}
|
||||
var hiding = false
|
||||
fun View.hide() {
|
||||
val target = -height.toFloat()
|
||||
if (hiding || translationY == target) return
|
||||
animate()
|
||||
.translationY(target)
|
||||
.setInterpolator(AccelerateInterpolator(2F))
|
||||
.setListener(object : AnimatorListenerAdapter() {
|
||||
override fun onAnimationEnd(animation: Animator?) {
|
||||
hiding = false
|
||||
}
|
||||
})
|
||||
|
||||
switch_per_app_proxy.setOnCheckedChangeListener { buttonView, isChecked ->
|
||||
defaultDPreference.setPrefBoolean(SettingsActivity.PREF_PER_APP_PROXY, isChecked)
|
||||
hiding = true
|
||||
}
|
||||
switch_per_app_proxy.isChecked = defaultDPreference.getPrefBoolean(SettingsActivity.PREF_PER_APP_PROXY, false)
|
||||
|
||||
switch_bypass_apps.setOnCheckedChangeListener { buttonView, isChecked ->
|
||||
defaultDPreference.setPrefBoolean(PREF_BYPASS_APPS, isChecked)
|
||||
var showing = false
|
||||
fun View.show() {
|
||||
val target = 0f
|
||||
if (showing || translationY == target) return
|
||||
animate()
|
||||
.translationY(target)
|
||||
.setInterpolator(DecelerateInterpolator(2F))
|
||||
.setListener(object : AnimatorListenerAdapter() {
|
||||
override fun onAnimationEnd(animation: Animator?) {
|
||||
showing = false
|
||||
}
|
||||
switch_bypass_apps.isChecked = defaultDPreference.getPrefBoolean(PREF_BYPASS_APPS, false)
|
||||
})
|
||||
showing = true
|
||||
}
|
||||
})
|
||||
***/
|
||||
|
||||
binding.switchPerAppProxy.setOnCheckedChangeListener { _, isChecked ->
|
||||
defaultSharedPreferences.edit().putBoolean(AppConfig.PREF_PER_APP_PROXY, isChecked).apply()
|
||||
}
|
||||
binding.switchPerAppProxy.isChecked = defaultSharedPreferences.getBoolean(AppConfig.PREF_PER_APP_PROXY, false)
|
||||
|
||||
binding.switchBypassApps.setOnCheckedChangeListener { _, isChecked ->
|
||||
defaultSharedPreferences.edit().putBoolean(AppConfig.PREF_BYPASS_APPS, isChecked).apply()
|
||||
}
|
||||
binding.switchBypassApps.isChecked = defaultSharedPreferences.getBoolean(AppConfig.PREF_BYPASS_APPS, false)
|
||||
|
||||
/***
|
||||
et_search.setOnEditorActionListener { v, actionId, event ->
|
||||
if (actionId == EditorInfo.IME_ACTION_SEARCH) {
|
||||
//hide
|
||||
var imm: InputMethodManager = v.context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||
imm.toggleSoftInput(0, InputMethodManager.HIDE_NOT_ALWAYS)
|
||||
if (actionId == EditorInfo.IME_ACTION_SEARCH) {
|
||||
//hide
|
||||
var imm: InputMethodManager = v.context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||
imm.toggleSoftInput(0, InputMethodManager.HIDE_NOT_ALWAYS)
|
||||
|
||||
val key = v.text.toString().toUpperCase()
|
||||
val apps = ArrayList<AppInfo>()
|
||||
if (TextUtils.isEmpty(key)) {
|
||||
appsAll?.forEach {
|
||||
apps.add(it)
|
||||
}
|
||||
} else {
|
||||
appsAll?.forEach {
|
||||
if (it.appName.toUpperCase().indexOf(key) >= 0) {
|
||||
apps.add(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
adapter = PerAppProxyAdapter(this, apps, adapter?.blacklist)
|
||||
recycler_view.adapter = adapter
|
||||
adapter?.notifyDataSetChanged()
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
val key = v.text.toString().toUpperCase()
|
||||
val apps = ArrayList<AppInfo>()
|
||||
if (TextUtils.isEmpty(key)) {
|
||||
appsAll?.forEach {
|
||||
apps.add(it)
|
||||
}
|
||||
} else {
|
||||
appsAll?.forEach {
|
||||
if (it.appName.toUpperCase().indexOf(key) >= 0) {
|
||||
apps.add(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
adapter = PerAppProxyAdapter(this, apps, adapter?.blacklist)
|
||||
recycler_view.adapter = adapter
|
||||
adapter?.notifyDataSetChanged()
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
***/
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
adapter?.let {
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -205,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)
|
||||
@@ -221,27 +241,41 @@ class PerAppProxyActivity : BaseActivity() {
|
||||
private fun selectProxyApp() {
|
||||
toast(R.string.msg_downloading_content)
|
||||
val url = AppConfig.androidpackagenamelistUrl
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
val content = try {
|
||||
URL(url).readText()
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
""
|
||||
}
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
val content = Utils.getUrlContext(url, 5000)
|
||||
launch(Dispatchers.Main) {
|
||||
Log.d("selectProxyApp", content)
|
||||
selectProxyApp(content)
|
||||
Log.d(ANG_PACKAGE, content)
|
||||
selectProxyApp(content, true)
|
||||
toast(R.string.toast_success)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun selectProxyApp(content: String): Boolean {
|
||||
private fun importProxyApp() {
|
||||
val content = Utils.getClipboard(applicationContext)
|
||||
if (TextUtils.isEmpty(content)) {
|
||||
return
|
||||
}
|
||||
selectProxyApp(content, false)
|
||||
toast(R.string.toast_success)
|
||||
}
|
||||
|
||||
private fun exportProxyApp() {
|
||||
var lst = binding.switchBypassApps.isChecked.toString()
|
||||
|
||||
adapter?.blacklist?.forEach block@{
|
||||
lst = lst + System.getProperty("line.separator") + it
|
||||
}
|
||||
Utils.setClipboard(applicationContext, lst)
|
||||
toast(R.string.toast_success)
|
||||
}
|
||||
|
||||
private fun selectProxyApp(content: String, force: Boolean): Boolean {
|
||||
try {
|
||||
var proxyApps = content
|
||||
if (TextUtils.isEmpty(content)) {
|
||||
val assets = Utils.readTextFromAssets(v2RayApplication, "proxy_packagename.txt")
|
||||
proxyApps = assets.lines().toString()
|
||||
val proxyApps = if (TextUtils.isEmpty(content)) {
|
||||
Utils.readTextFromAssets(v2RayApplication, "proxy_packagename.txt")
|
||||
} else {
|
||||
content
|
||||
}
|
||||
if (TextUtils.isEmpty(proxyApps)) {
|
||||
return false
|
||||
@@ -249,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
|
||||
@@ -266,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
|
||||
@@ -282,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,13 +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 java.util.*
|
||||
|
||||
class PerAppProxyAdapter(val activity: BaseActivity, val apps: List<AppInfo>, blacklist: MutableSet<String>?) :
|
||||
@@ -18,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) {
|
||||
@@ -37,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(LayoutInflater.from(ctx)
|
||||
.inflate(R.layout.item_recycler_bypass_list, parent, false))
|
||||
else -> AppViewHolder(ItemRecyclerBypassListBinding.inflate(LayoutInflater.from(ctx), parent, false))
|
||||
|
||||
}
|
||||
}
|
||||
@@ -53,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.setImageDrawable(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.setTextColor(Color.RED)
|
||||
itemBypassBinding.name.text = String.format("** %1s", appInfo.appName)
|
||||
//name.textColor = Color.RED
|
||||
} else {
|
||||
name.text = appInfo.appName
|
||||
name.setTextColor(Color.DKGRAY)
|
||||
itemBypassBinding.name.text = appInfo.appName
|
||||
//name.textColor = Color.DKGRAY
|
||||
}
|
||||
|
||||
itemView.setOnClickListener(this)
|
||||
@@ -85,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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,32 +4,36 @@ 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.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 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 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
|
||||
import java.net.URL
|
||||
|
||||
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 {
|
||||
@@ -40,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)
|
||||
}
|
||||
@@ -56,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 -> {
|
||||
@@ -80,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(Intent(activity, ScannerActivity::class.java), 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)
|
||||
}
|
||||
@@ -98,51 +109,50 @@ class RoutingSettingsFragment : Fragment() {
|
||||
return true
|
||||
}
|
||||
|
||||
private val scanQRCodeForReplace = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||
if (it.resultCode == RESULT_OK) {
|
||||
val content = it.data?.getStringExtra("SCAN_RESULT")
|
||||
binding.etRoutingContent.text = Utils.getEditable(content!!)
|
||||
}
|
||||
}
|
||||
|
||||
private val scanQRCodeForAppend = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||
if (it.resultCode == RESULT_OK) {
|
||||
val content = it.data?.getStringExtra("SCAN_RESULT")
|
||||
binding.etRoutingContent.text = Utils.getEditable("${binding.etRoutingContent.text},$content")
|
||||
}
|
||||
}
|
||||
|
||||
fun setDefaultRules(): Boolean {
|
||||
var url = AppConfig.v2rayCustomRoutingListUrl
|
||||
when (arguments!!.getString(routing_arg)) {
|
||||
var tag = ""
|
||||
when (requireArguments().getString(routing_arg)) {
|
||||
AppConfig.PREF_V2RAY_ROUTING_AGENT -> {
|
||||
url += AppConfig.TAG_AGENT
|
||||
tag = AppConfig.TAG_AGENT
|
||||
}
|
||||
AppConfig.PREF_V2RAY_ROUTING_DIRECT -> {
|
||||
url += AppConfig.TAG_DIRECT
|
||||
tag = AppConfig.TAG_DIRECT
|
||||
}
|
||||
AppConfig.PREF_V2RAY_ROUTING_BLOCKED -> {
|
||||
url += AppConfig.TAG_BLOCKED
|
||||
tag = AppConfig.TAG_BLOCKED
|
||||
}
|
||||
}
|
||||
url += tag
|
||||
|
||||
activity?.toast(R.string.msg_downloading_content)
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
val content = try {
|
||||
URL(url).readText()
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
""
|
||||
}
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
val content = Utils.getUrlContext(url, 5000)
|
||||
launch(Dispatchers.Main) {
|
||||
et_routing_content.text = Utils.getEditable(content)
|
||||
activity?.toast(R.string.toast_success)
|
||||
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 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(Intent(this, ScannerActivity::class.java), 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"), "")
|
||||
if (count > 0) {
|
||||
toast(R.string.toast_success)
|
||||
} else {
|
||||
toast(R.string.toast_failure)
|
||||
}
|
||||
startActivity(Intent(this, MainActivity::class.java))
|
||||
}
|
||||
private val scanQRCode = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||
if (it.resultCode == RESULT_OK) {
|
||||
val count = AngConfigManager.importBatchConfig(it.data?.getStringExtra("SCAN_RESULT"), "", false)
|
||||
if (count > 0) {
|
||||
toast(R.string.toast_success)
|
||||
} else {
|
||||
toast(R.string.toast_failure)
|
||||
}
|
||||
startActivity(Intent(this, MainActivity::class.java))
|
||||
}
|
||||
finish()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import android.content.Intent
|
||||
import android.graphics.BitmapFactory
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import com.google.zxing.BarcodeFormat
|
||||
import com.tbruyelle.rxpermissions.RxPermissions
|
||||
import com.v2ray.ang.R
|
||||
@@ -16,15 +17,11 @@ import com.v2ray.ang.extension.toast
|
||||
import com.v2ray.ang.util.QRCodeDecoder
|
||||
|
||||
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)
|
||||
@@ -59,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
|
||||
}
|
||||
@@ -97,30 +94,22 @@ 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 -> {
|
||||
val uri = data?.data
|
||||
if (resultCode == RESULT_OK && uri != null) {
|
||||
try {
|
||||
val bitmap = BitmapFactory.decodeStream(contentResolver.openInputStream(uri))
|
||||
val text = QRCodeDecoder.syncDecodeQRCode(bitmap)
|
||||
finished(text)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
toast(e.message.toString())
|
||||
}
|
||||
}
|
||||
private val chooseFile = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||
val uri = it.data?.data
|
||||
if (it.resultCode == RESULT_OK && uri != null) {
|
||||
try {
|
||||
val bitmap = BitmapFactory.decodeStream(contentResolver.openInputStream(uri))
|
||||
val text = QRCodeDecoder.syncDecodeQRCode(bitmap)
|
||||
finished(text!!)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
toast(e.message.toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,166 +0,0 @@
|
||||
package com.v2ray.ang.ui
|
||||
|
||||
import android.os.Bundle
|
||||
import android.support.v7.app.AlertDialog
|
||||
import android.text.Editable
|
||||
import android.text.TextUtils
|
||||
import android.util.Log
|
||||
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.extension.toast
|
||||
import com.v2ray.ang.util.AngConfigManager
|
||||
import com.v2ray.ang.util.Utils
|
||||
import kotlinx.android.synthetic.main.activity_server2.*
|
||||
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())
|
||||
if (edit_index == configs.index) {
|
||||
if (!AngConfigManager.genStoreV2rayConfig(edit_index)) {
|
||||
Log.d(AppConfig.ANG_PACKAGE, "update custom config $edit_index but generate full configuration failed!")
|
||||
}
|
||||
}
|
||||
finish()
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* save server config
|
||||
*/
|
||||
fun deleteServer(): Boolean {
|
||||
if (edit_index >= 0) {
|
||||
AlertDialog.Builder(this).setMessage(R.string.del_config_comfirm)
|
||||
.setPositiveButton(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,174 +0,0 @@
|
||||
package com.v2ray.ang.ui
|
||||
|
||||
import android.os.Bundle
|
||||
import android.support.v7.app.AlertDialog
|
||||
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.extension.toast
|
||||
import com.v2ray.ang.util.AngConfigManager
|
||||
import com.v2ray.ang.util.Utils
|
||||
import kotlinx.android.synthetic.main.activity_server3.*
|
||||
|
||||
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) {
|
||||
AlertDialog.Builder(this).setMessage(R.string.del_config_comfirm)
|
||||
.setPositiveButton(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,158 +0,0 @@
|
||||
package com.v2ray.ang.ui
|
||||
|
||||
import android.os.Bundle
|
||||
import android.support.v7.app.AlertDialog
|
||||
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.extension.toast
|
||||
import com.v2ray.ang.util.AngConfigManager
|
||||
import com.v2ray.ang.util.Utils
|
||||
import kotlinx.android.synthetic.main.activity_server4.*
|
||||
|
||||
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) {
|
||||
AlertDialog.Builder(this).setMessage(R.string.del_config_comfirm)
|
||||
.setPositiveButton(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,54 +1,133 @@
|
||||
package com.v2ray.ang.ui
|
||||
|
||||
import android.os.Bundle
|
||||
import android.support.v7.app.AlertDialog
|
||||
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.dto.EConfigType
|
||||
import com.v2ray.ang.dto.ServerConfig
|
||||
import com.v2ray.ang.dto.V2rayConfig
|
||||
import com.v2ray.ang.dto.V2rayConfig.Companion.DEFAULT_PORT
|
||||
import com.v2ray.ang.extension.toast
|
||||
import com.v2ray.ang.util.AngConfigManager
|
||||
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.*
|
||||
|
||||
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 +137,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,112 +197,202 @@ 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) {
|
||||
toast(R.string.server_lab_alterid)
|
||||
return false
|
||||
sp_stream_security?.let {
|
||||
if (config.configType == EConfigType.TROJAN && TextUtils.isEmpty(streamSecuritys[it.selectedItemPosition])) {
|
||||
toast(R.string.server_lab_stream_security)
|
||||
return false
|
||||
}
|
||||
}
|
||||
et_alterId?.let {
|
||||
val alterId = Utils.parseInt(it.text.toString())
|
||||
if (alterId < 0) {
|
||||
toast(R.string.server_lab_alterid)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
if (AngConfigManager.addServer(vmess, edit_index) == 0) {
|
||||
toast(R.string.toast_success)
|
||||
finish()
|
||||
return true
|
||||
config.remarks = et_remarks.text.toString().trim()
|
||||
config.outboundBean?.settings?.vnext?.get(0)?.let { vnext ->
|
||||
saveVnext(vnext, port, config)
|
||||
}
|
||||
config.outboundBean?.settings?.servers?.get(0)?.let { server ->
|
||||
saveServers(server, port, config)
|
||||
}
|
||||
config.outboundBean?.streamSettings?.let {
|
||||
saveStreamSettings(it)
|
||||
}
|
||||
if(config.subscriptionId.isEmpty() && !subscriptionId.isNullOrEmpty()) {
|
||||
config.subscriptionId = subscriptionId!!
|
||||
}
|
||||
|
||||
MmkvManager.encodeServerConfig(editGuid, config)
|
||||
toast(R.string.toast_success)
|
||||
finish()
|
||||
return true
|
||||
}
|
||||
|
||||
private fun saveVnext(vnext: V2rayConfig.OutboundBean.OutSettingsBean.VnextBean, port: Int, config: ServerConfig) {
|
||||
vnext.address = et_address.text.toString().trim()
|
||||
vnext.port = port
|
||||
vnext.users[0].id = et_id.text.toString().trim()
|
||||
if (config.configType == EConfigType.VMESS) {
|
||||
vnext.users[0].alterId = Utils.parseInt(et_alterId?.text.toString())
|
||||
vnext.users[0].security = securitys[sp_security?.selectedItemPosition ?: 0]
|
||||
} else if (config.configType == EConfigType.VLESS) {
|
||||
vnext.users[0].encryption = et_security?.text.toString().trim()
|
||||
vnext.users[0].flow = flows[sp_flow?.selectedItemPosition ?: 0]
|
||||
}
|
||||
}
|
||||
|
||||
private fun saveServers(server: V2rayConfig.OutboundBean.OutSettingsBean.ServersBean, port: Int, config: ServerConfig) {
|
||||
server.address = et_address.text.toString().trim()
|
||||
server.port = port
|
||||
if (config.configType == EConfigType.SHADOWSOCKS) {
|
||||
server.password = et_id.text.toString().trim()
|
||||
server.method = shadowsocksSecuritys[sp_security?.selectedItemPosition ?: 0]
|
||||
} else if (config.configType == EConfigType.SOCKS) {
|
||||
if (TextUtils.isEmpty(et_security?.text) && TextUtils.isEmpty(et_id.text)) {
|
||||
server.users = null
|
||||
} else {
|
||||
val socksUsersBean = V2rayConfig.OutboundBean.OutSettingsBean.ServersBean.SocksUsersBean()
|
||||
socksUsersBean.user = et_security?.text.toString().trim()
|
||||
socksUsersBean.pass = et_id.text.toString().trim()
|
||||
server.users = listOf(socksUsersBean)
|
||||
}
|
||||
} else if (config.configType == EConfigType.TROJAN) {
|
||||
server.password = et_id.text.toString().trim()
|
||||
server.flow =
|
||||
if (streamSecuritys[sp_stream_security?.selectedItemPosition ?: 0] == V2rayConfig.XTLS) {
|
||||
flows[sp_flow?.selectedItemPosition ?: 0]
|
||||
} else {
|
||||
""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun saveStreamSettings(streamSetting: V2rayConfig.OutboundBean.StreamSettingsBean) {
|
||||
val network = sp_network?.selectedItemPosition ?: return
|
||||
val type = sp_header_type?.selectedItemPosition ?: return
|
||||
val requestHost = et_request_host?.text?.toString()?.trim() ?: return
|
||||
val path = et_path?.text?.toString()?.trim() ?: return
|
||||
val sniField = et_sni?.text?.toString()?.trim() ?: return
|
||||
val allowInsecureField = sp_allow_insecure?.selectedItemPosition ?: return
|
||||
val streamSecurity = sp_stream_security?.selectedItemPosition ?: return
|
||||
var utlsIndex = sp_stream_fingerprint?.selectedItemPosition ?: return
|
||||
var alpnIndex = sp_stream_alpn?.selectedItemPosition ?: return
|
||||
|
||||
var sni = streamSetting.populateTransportSettings(
|
||||
transport = networks[network],
|
||||
headerType = transportTypes(networks[network])[type],
|
||||
host = requestHost,
|
||||
path = path,
|
||||
seed = path,
|
||||
quicSecurity = requestHost,
|
||||
key = path,
|
||||
mode = transportTypes(networks[network])[type],
|
||||
serviceName = path
|
||||
)
|
||||
if (sniField.isNotBlank()) {
|
||||
sni = sniField
|
||||
}
|
||||
val allowInsecure = if (allowinsecures[allowInsecureField].isBlank()) {
|
||||
settingsStorage?.decodeBool(PREF_ALLOW_INSECURE) ?: false
|
||||
} else {
|
||||
toast(R.string.toast_failure)
|
||||
return false
|
||||
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) {
|
||||
AlertDialog.Builder(this).setMessage(R.string.del_config_comfirm)
|
||||
.setPositiveButton(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()
|
||||
} else {
|
||||
toast(R.string.toast_failure)
|
||||
}
|
||||
}
|
||||
.show()
|
||||
} else {
|
||||
.show()
|
||||
} else {
|
||||
MmkvManager.removeServer(editGuid)
|
||||
finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
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,45 +1,18 @@
|
||||
package com.v2ray.ang.ui
|
||||
|
||||
import android.arch.lifecycle.ViewModelProviders
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.support.v7.preference.*
|
||||
import android.text.TextUtils
|
||||
import android.view.View
|
||||
import com.v2ray.ang.R
|
||||
import androidx.activity.viewModels
|
||||
import androidx.preference.*
|
||||
import com.v2ray.ang.AppConfig
|
||||
import com.v2ray.ang.extension.toast
|
||||
import com.v2ray.ang.util.AngConfigManager
|
||||
import com.v2ray.ang.R
|
||||
import com.v2ray.ang.util.Utils
|
||||
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_AUTO_RESTART = "pref_auto_restart"
|
||||
const val PREF_FORWARD_IPV6 = "pref_forward_ipv6"
|
||||
}
|
||||
|
||||
private val settingsViewModel by lazy { ViewModelProviders.of(this).get(SettingsViewModel::class.java) }
|
||||
private val settingsViewModel: SettingsViewModel by viewModels()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
@@ -53,130 +26,31 @@ class SettingsActivity : BaseActivity() {
|
||||
}
|
||||
|
||||
class SettingsFragment : PreferenceFragmentCompat() {
|
||||
private val perAppProxy by lazy { findPreference(PREF_PER_APP_PROXY) as CheckBoxPreference }
|
||||
private val sppedEnabled by lazy { findPreference(PREF_SPEED_ENABLED) as CheckBoxPreference }
|
||||
private val sniffingEnabled by lazy { findPreference(PREF_SNIFFING_ENABLED) as CheckBoxPreference }
|
||||
private val proxySharing by lazy { findPreference(PREF_PROXY_SHARING) as CheckBoxPreference }
|
||||
private val domainStrategy by lazy { findPreference(PREF_ROUTING_DOMAIN_STRATEGY) as ListPreference }
|
||||
private val routingMode by lazy { findPreference(PREF_ROUTING_MODE) as ListPreference }
|
||||
|
||||
private val forwardIpv6 by lazy { findPreference(PREF_FORWARD_IPV6) as CheckBoxPreference }
|
||||
private val enableLocalDns by lazy { findPreference(PREF_LOCAL_DNS_ENABLED) as CheckBoxPreference }
|
||||
private val domesticDns by lazy { findPreference(PREF_DOMESTIC_DNS) as EditTextPreference }
|
||||
private val remoteDns by lazy { findPreference(PREF_REMOTE_DNS) as EditTextPreference }
|
||||
|
||||
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 }
|
||||
|
||||
private 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) }
|
||||
|
||||
private val mode by lazy { findPreference(AppConfig.PREF_MODE) as ListPreference }
|
||||
|
||||
private fun restartProxy() {
|
||||
Utils.stopVService(requireContext())
|
||||
Utils.startVService(requireContext(), AngConfigManager.configs.index)
|
||||
}
|
||||
|
||||
private fun isRunning(): Boolean {
|
||||
return false //TODO no point of adding logic now since Settings will be changed soon
|
||||
}
|
||||
private val mode by lazy { findPreference<ListPreference>(AppConfig.PREF_MODE) }
|
||||
|
||||
override fun onCreatePreferences(bundle: Bundle?, s: String?) {
|
||||
addPreferencesFromResource(R.xml.pref_settings)
|
||||
|
||||
perAppProxy.setOnPreferenceClickListener {
|
||||
if (isRunning()) {
|
||||
Utils.stopVService(requireContext())
|
||||
}
|
||||
startActivity(Intent(activity, PerAppProxyActivity::class.java))
|
||||
perAppProxy.isChecked = true
|
||||
true
|
||||
}
|
||||
sppedEnabled.setOnPreferenceClickListener {
|
||||
if (isRunning())
|
||||
restartProxy()
|
||||
true
|
||||
}
|
||||
sniffingEnabled.setOnPreferenceClickListener {
|
||||
if (isRunning())
|
||||
restartProxy()
|
||||
true
|
||||
}
|
||||
|
||||
proxySharing.setOnPreferenceClickListener {
|
||||
if (proxySharing.isChecked)
|
||||
activity?.toast(R.string.toast_warning_pref_proxysharing)
|
||||
if (isRunning())
|
||||
restartProxy()
|
||||
true
|
||||
}
|
||||
|
||||
domainStrategy.setOnPreferenceChangeListener { _, _ ->
|
||||
if (isRunning())
|
||||
restartProxy()
|
||||
true
|
||||
}
|
||||
routingMode.setOnPreferenceChangeListener { _, _ ->
|
||||
if (isRunning())
|
||||
restartProxy()
|
||||
true
|
||||
}
|
||||
|
||||
routingCustom.setOnPreferenceClickListener {
|
||||
if (isRunning())
|
||||
Utils.stopVService(requireContext())
|
||||
routingCustom?.setOnPreferenceClickListener {
|
||||
startActivity(Intent(activity, RoutingSettingsActivity::class.java))
|
||||
false
|
||||
}
|
||||
|
||||
forwardIpv6.setOnPreferenceClickListener {
|
||||
if (isRunning())
|
||||
restartProxy()
|
||||
true
|
||||
}
|
||||
|
||||
enableLocalDns.setOnPreferenceClickListener {
|
||||
if (isRunning())
|
||||
restartProxy()
|
||||
true
|
||||
}
|
||||
|
||||
|
||||
domesticDns.setOnPreferenceChangeListener { _, 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 { _, any ->
|
||||
// remoteDns.summary = any as String
|
||||
val nval = any as String
|
||||
remoteDns.summary = if (nval == "") AppConfig.DNS_AGENT else nval
|
||||
if (isRunning())
|
||||
restartProxy()
|
||||
true
|
||||
}
|
||||
|
||||
mode.setOnPreferenceChangeListener { _, newValue ->
|
||||
updatePerAppProxy(newValue.toString())
|
||||
true
|
||||
}
|
||||
mode.dialogLayoutResource = R.layout.preference_with_help_link
|
||||
|
||||
// donate.onClick {
|
||||
// startActivity<InappBuyActivity>()
|
||||
// }
|
||||
|
||||
// licenses.onClick {
|
||||
// val fragment = LicensesDialogFragment.Builder(act)
|
||||
// .setNotices(R.raw.licenses)
|
||||
@@ -199,46 +73,107 @@ 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
|
||||
}
|
||||
|
||||
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(activity)
|
||||
updatePerAppProxy(defaultSharedPreferences.getString(AppConfig.PREF_MODE, "VPN"))
|
||||
remoteDns.summary = defaultSharedPreferences.getString(PREF_REMOTE_DNS, "")
|
||||
domesticDns.summary = defaultSharedPreferences.getString(PREF_DOMESTIC_DNS, "")
|
||||
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, "")
|
||||
|
||||
if (remoteDns.summary == "") {
|
||||
remoteDns.summary = AppConfig.DNS_AGENT
|
||||
localDnsPort?.summary = defaultSharedPreferences.getString(AppConfig.PREF_LOCAL_DNS_PORT, AppConfig.PORT_LOCAL_DNS)
|
||||
socksPort?.summary = defaultSharedPreferences.getString(AppConfig.PREF_SOCKS_PORT, AppConfig.PORT_SOCKS)
|
||||
httpPort?.summary = defaultSharedPreferences.getString(AppConfig.PREF_HTTP_PORT, AppConfig.PORT_HTTP)
|
||||
|
||||
if (TextUtils.isEmpty(remoteDnsString)) {
|
||||
remoteDnsString = AppConfig.DNS_AGENT
|
||||
}
|
||||
|
||||
if ( 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, "")
|
||||
if (TextUtils.isEmpty(localDnsPort?.summary)) {
|
||||
localDnsPort?.summary = AppConfig.PORT_LOCAL_DNS
|
||||
}
|
||||
if (TextUtils.isEmpty(socksPort?.summary)) {
|
||||
socksPort?.summary = AppConfig.PORT_SOCKS
|
||||
}
|
||||
if (TextUtils.isEmpty(httpPort?.summary)) {
|
||||
httpPort?.summary = AppConfig.PORT_HTTP
|
||||
}
|
||||
}
|
||||
|
||||
private fun updatePerAppProxy(mode: String?) {
|
||||
if (mode == "VPN") {
|
||||
perAppProxy.isEnabled = true
|
||||
perAppProxy.isChecked = PreferenceManager.getDefaultSharedPreferences(activity)
|
||||
.getBoolean(PREF_PER_APP_PROXY, false)
|
||||
} else {
|
||||
perAppProxy.isEnabled = false
|
||||
perAppProxy.isChecked = false
|
||||
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) {
|
||||
|
||||
@@ -1,36 +1,38 @@
|
||||
package com.v2ray.ang.ui
|
||||
|
||||
import android.os.Bundle
|
||||
import android.support.v7.app.AlertDialog
|
||||
import android.text.TextUtils
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import com.google.gson.Gson
|
||||
import com.tencent.mmkv.MMKV
|
||||
import com.v2ray.ang.R
|
||||
import com.v2ray.ang.dto.AngConfig
|
||||
import com.v2ray.ang.databinding.ActivitySubEditBinding
|
||||
import com.v2ray.ang.dto.SubscriptionItem
|
||||
import com.v2ray.ang.extension.toast
|
||||
import com.v2ray.ang.util.AngConfigManager
|
||||
import com.v2ray.ang.util.MmkvManager
|
||||
import com.v2ray.ang.util.Utils
|
||||
import kotlinx.android.synthetic.main.activity_sub_edit.*
|
||||
|
||||
class SubEditActivity : BaseActivity() {
|
||||
private lateinit var binding: ActivitySubEditBinding
|
||||
|
||||
var del_config: MenuItem? = null
|
||||
var save_config: MenuItem? = null
|
||||
|
||||
private lateinit var configs: AngConfig
|
||||
private var edit_index: Int = -1 //当前编辑的
|
||||
private val subStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_SUB, MMKV.MULTI_PROCESS_MODE) }
|
||||
private val editSubId by lazy { intent.getStringExtra("subId").orEmpty() }
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_sub_edit)
|
||||
|
||||
configs = AngConfigManager.configs
|
||||
edit_index = intent.getIntExtra("position", -1)
|
||||
|
||||
binding = ActivitySubEditBinding.inflate(layoutInflater)
|
||||
val view = binding.root
|
||||
setContentView(view)
|
||||
title = getString(R.string.title_sub_setting)
|
||||
|
||||
if (edit_index >= 0) {
|
||||
bindingServer(configs.subItem[edit_index])
|
||||
val json = subStorage?.decodeString(editSubId)
|
||||
if (!json.isNullOrBlank()) {
|
||||
bindingServer(Gson().fromJson(json, SubscriptionItem::class.java))
|
||||
} else {
|
||||
clearServer()
|
||||
}
|
||||
@@ -40,83 +42,77 @@ class SubEditActivity : BaseActivity() {
|
||||
/**
|
||||
* bingding seleced server config
|
||||
*/
|
||||
fun bindingServer(subItem: AngConfig.SubItemBean): Boolean {
|
||||
et_remarks.text = Utils.getEditable(subItem.remarks)
|
||||
et_url.text = Utils.getEditable(subItem.url)
|
||||
|
||||
private fun bindingServer(subItem: SubscriptionItem): Boolean {
|
||||
binding.etRemarks.text = Utils.getEditable(subItem.remarks)
|
||||
binding.etUrl.text = Utils.getEditable(subItem.url)
|
||||
binding.chkEnable.isChecked = subItem.enabled
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* clear or init server config
|
||||
*/
|
||||
fun clearServer(): Boolean {
|
||||
et_remarks.text = null
|
||||
et_url.text = null
|
||||
|
||||
private fun clearServer(): Boolean {
|
||||
binding.etRemarks.text = null
|
||||
binding.etUrl.text = null
|
||||
binding.chkEnable.isChecked = true
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* save server config
|
||||
*/
|
||||
fun saveServer(): Boolean {
|
||||
val subItem: AngConfig.SubItemBean
|
||||
if (edit_index >= 0) {
|
||||
subItem = configs.subItem[edit_index]
|
||||
private fun saveServer(): Boolean {
|
||||
val subItem: SubscriptionItem
|
||||
val json = subStorage?.decodeString(editSubId)
|
||||
var subId = editSubId
|
||||
if (!json.isNullOrBlank()) {
|
||||
subItem = Gson().fromJson(json, SubscriptionItem::class.java)
|
||||
} else {
|
||||
subItem = AngConfig.SubItemBean()
|
||||
subId = Utils.getUuid()
|
||||
subItem = SubscriptionItem()
|
||||
}
|
||||
|
||||
subItem.remarks = et_remarks.text.toString()
|
||||
subItem.url = et_url.text.toString()
|
||||
subItem.remarks = binding.etRemarks.text.toString()
|
||||
subItem.url = binding.etUrl.text.toString()
|
||||
subItem.enabled = binding.chkEnable.isChecked
|
||||
|
||||
if (TextUtils.isEmpty(subItem.remarks)) {
|
||||
toast(R.string.sub_setting_remarks)
|
||||
return false
|
||||
}
|
||||
if (TextUtils.isEmpty(subItem.url)) {
|
||||
toast(R.string.sub_setting_url)
|
||||
return false
|
||||
}
|
||||
// if (TextUtils.isEmpty(subItem.url)) {
|
||||
// toast(R.string.sub_setting_url)
|
||||
// return false
|
||||
// }
|
||||
|
||||
if (AngConfigManager.addSubItem(subItem, edit_index) == 0) {
|
||||
toast(R.string.toast_success)
|
||||
finish()
|
||||
return true
|
||||
} else {
|
||||
toast(R.string.toast_failure)
|
||||
return false
|
||||
}
|
||||
subStorage?.encode(subId, Gson().toJson(subItem))
|
||||
toast(R.string.toast_success)
|
||||
finish()
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* save server config
|
||||
*/
|
||||
fun deleteServer(): Boolean {
|
||||
if (edit_index >= 0) {
|
||||
private fun deleteServer(): Boolean {
|
||||
if (editSubId.isNotEmpty()) {
|
||||
AlertDialog.Builder(this).setMessage(R.string.del_config_comfirm)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
if (AngConfigManager.removeSubItem(edit_index) == 0) {
|
||||
toast(R.string.toast_success)
|
||||
finish()
|
||||
} else {
|
||||
toast(R.string.toast_failure)
|
||||
}
|
||||
MmkvManager.removeSubscription(editSubId)
|
||||
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)
|
||||
del_config = menu.findItem(R.id.del_config)
|
||||
save_config = menu.findItem(R.id.save_config)
|
||||
|
||||
if (edit_index >= 0) {
|
||||
} else {
|
||||
if (editSubId.isEmpty()) {
|
||||
del_config?.isVisible = false
|
||||
}
|
||||
|
||||
|
||||
@@ -1,53 +1,55 @@
|
||||
package com.v2ray.ang.ui
|
||||
|
||||
import android.content.Intent
|
||||
import android.support.v7.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import com.v2ray.ang.R
|
||||
import kotlinx.android.synthetic.main.activity_sub_setting.*
|
||||
import android.os.Bundle
|
||||
import com.v2ray.ang.databinding.ActivitySubSettingBinding
|
||||
import com.v2ray.ang.dto.SubscriptionItem
|
||||
import com.v2ray.ang.util.MmkvManager
|
||||
|
||||
class SubSettingActivity : BaseActivity() {
|
||||
private lateinit var binding: ActivitySubSettingBinding
|
||||
|
||||
var subscriptions:List<Pair<String, SubscriptionItem>> = listOf()
|
||||
private val adapter by lazy { SubSettingRecyclerAdapter(this) }
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_sub_setting)
|
||||
binding = ActivitySubSettingBinding.inflate(layoutInflater)
|
||||
val view = binding.root
|
||||
setContentView(view)
|
||||
|
||||
title = getString(R.string.title_sub_setting)
|
||||
|
||||
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
|
||||
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
adapter.updateConfigList()
|
||||
subscriptions = MmkvManager.decodeSubscriptions()
|
||||
adapter.notifyDataSetChanged()
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
|
||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||
menuInflater.inflate(R.menu.action_sub_setting, menu)
|
||||
menu?.findItem(R.id.del_config)?.isVisible = false
|
||||
menu?.findItem(R.id.save_config)?.isVisible = false
|
||||
menu.findItem(R.id.del_config)?.isVisible = false
|
||||
menu.findItem(R.id.save_config)?.isVisible = false
|
||||
|
||||
return super.onCreateOptionsMenu(menu)
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
|
||||
R.id.add_config -> {
|
||||
startActivity(Intent(this, SubEditActivity::class.java)
|
||||
.putExtra("position", -1)
|
||||
)
|
||||
adapter.updateConfigList()
|
||||
startActivity(Intent(this, SubEditActivity::class.java))
|
||||
true
|
||||
}
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -2,64 +2,49 @@ package com.v2ray.ang.ui
|
||||
|
||||
import android.content.Intent
|
||||
import android.graphics.Color
|
||||
import android.support.v7.widget.RecyclerView
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import android.view.ViewGroup
|
||||
import com.google.gson.Gson
|
||||
import com.tencent.mmkv.MMKV
|
||||
import com.v2ray.ang.R
|
||||
import com.v2ray.ang.dto.AngConfig
|
||||
import com.v2ray.ang.util.AngConfigManager
|
||||
import kotlinx.android.synthetic.main.item_recycler_sub_setting.view.*
|
||||
import com.v2ray.ang.databinding.ItemRecyclerSubSettingBinding
|
||||
import com.v2ray.ang.util.MmkvManager
|
||||
|
||||
class SubSettingRecyclerAdapter(val activity: SubSettingActivity) : RecyclerView.Adapter<SubSettingRecyclerAdapter.BaseViewHolder>() {
|
||||
class SubSettingRecyclerAdapter(val activity: SubSettingActivity) : RecyclerView.Adapter<SubSettingRecyclerAdapter.MainViewHolder>() {
|
||||
|
||||
private var mActivity: SubSettingActivity = activity
|
||||
private lateinit var configs: AngConfig
|
||||
private val subStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_SUB, MMKV.MULTI_PROCESS_MODE) }
|
||||
|
||||
init {
|
||||
updateConfigList()
|
||||
}
|
||||
override fun getItemCount() = mActivity.subscriptions.size
|
||||
|
||||
override fun getItemCount() = configs.subItem.count()
|
||||
|
||||
override fun onBindViewHolder(holder: BaseViewHolder, position: Int) {
|
||||
if (holder is MainViewHolder) {
|
||||
val remarks = configs.subItem[position].remarks
|
||||
val url = configs.subItem[position].url
|
||||
|
||||
holder.name.text = remarks
|
||||
holder.url.text = url
|
||||
holder.itemView.setBackgroundColor(Color.TRANSPARENT)
|
||||
|
||||
holder.layout_edit.setOnClickListener {
|
||||
mActivity.startActivity(Intent(mActivity, SubEditActivity::class.java)
|
||||
.putExtra("position", position)
|
||||
)
|
||||
}
|
||||
override fun onBindViewHolder(holder: MainViewHolder, position: Int) {
|
||||
val subId = mActivity.subscriptions[position].first
|
||||
val subItem = mActivity.subscriptions[position].second
|
||||
holder.itemSubSettingBinding.tvName.text = subItem.remarks
|
||||
holder.itemSubSettingBinding.tvUrl.text = subItem.url
|
||||
if (subItem.enabled) {
|
||||
holder.itemSubSettingBinding.chkEnable.setBackgroundResource(R.color.colorSelected)
|
||||
} else {
|
||||
holder.itemSubSettingBinding.chkEnable.setBackgroundResource(R.color.colorUnselected)
|
||||
}
|
||||
holder.itemView.setBackgroundColor(Color.TRANSPARENT)
|
||||
|
||||
holder.itemSubSettingBinding.layoutEdit.setOnClickListener {
|
||||
mActivity.startActivity(Intent(mActivity, SubEditActivity::class.java)
|
||||
.putExtra("subId", subId)
|
||||
)
|
||||
}
|
||||
holder.itemSubSettingBinding.infoContainer.setOnClickListener {
|
||||
subItem.enabled = !subItem.enabled
|
||||
subStorage?.encode(subId, Gson().toJson(subItem))
|
||||
notifyItemChanged(position)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseViewHolder {
|
||||
return MainViewHolder(LayoutInflater.from(parent.context)
|
||||
.inflate(R.layout.item_recycler_sub_setting, parent, false))
|
||||
}
|
||||
|
||||
fun updateConfigList() {
|
||||
configs = AngConfigManager.configs
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
// fun updateSelectedItem() {
|
||||
// notifyItemChanged(configs.index)
|
||||
// }
|
||||
|
||||
open class BaseViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView)
|
||||
|
||||
class MainViewHolder(itemView: View) : BaseViewHolder(itemView) {
|
||||
val name = itemView.tv_name!!
|
||||
val url = itemView.tv_url!!
|
||||
val layout_edit = itemView.layout_edit!!
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MainViewHolder {
|
||||
return MainViewHolder(ItemRecyclerSubSettingBinding.inflate(LayoutInflater.from(parent.context), parent, false))
|
||||
}
|
||||
|
||||
class MainViewHolder(val itemSubSettingBinding: ItemRecyclerSubSettingBinding) : RecyclerView.ViewHolder(itemSubSettingBinding.root)
|
||||
}
|
||||
|
||||
@@ -7,32 +7,40 @@ import android.widget.ArrayAdapter
|
||||
import android.widget.ListView
|
||||
import java.util.ArrayList
|
||||
import com.v2ray.ang.R
|
||||
import com.v2ray.ang.util.AngConfigManager
|
||||
import android.content.Intent
|
||||
import android.text.TextUtils
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import com.google.zxing.WriterException
|
||||
import com.tencent.mmkv.MMKV
|
||||
import com.v2ray.ang.AppConfig
|
||||
import kotlinx.android.synthetic.main.activity_tasker.*
|
||||
|
||||
import com.v2ray.ang.databinding.ActivityTaskerBinding
|
||||
import com.v2ray.ang.util.MmkvManager
|
||||
|
||||
class TaskerActivity : BaseActivity() {
|
||||
private lateinit var binding: ActivityTaskerBinding
|
||||
|
||||
private var listview: ListView? = null
|
||||
private var lstData: ArrayList<String> = ArrayList()
|
||||
private var lstGuid: ArrayList<String> = ArrayList()
|
||||
|
||||
private val serverStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_SERVER_CONFIG, MMKV.MULTI_PROCESS_MODE) }
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_tasker)
|
||||
binding = ActivityTaskerBinding.inflate(layoutInflater)
|
||||
val view = binding.root
|
||||
setContentView(view)
|
||||
|
||||
//add def value
|
||||
lstData.add("Default")
|
||||
lstGuid.add(AppConfig.TASKER_DEFAULT_GUID)
|
||||
|
||||
AngConfigManager.configs.vmess.forEach {
|
||||
lstData.add(it.remarks)
|
||||
lstGuid.add(it.guid)
|
||||
serverStorage?.allKeys()?.forEach { key ->
|
||||
MmkvManager.decodeServerConfig(key)?.let { config ->
|
||||
lstData.add(config.remarks)
|
||||
lstGuid.add(key)
|
||||
}
|
||||
}
|
||||
val adapter = ArrayAdapter(this,
|
||||
android.R.layout.simple_list_item_single_choice, lstData)
|
||||
@@ -51,7 +59,7 @@ class TaskerActivity : BaseActivity() {
|
||||
if (switch == null || TextUtils.isEmpty(guid)) {
|
||||
return
|
||||
} else {
|
||||
switch_start_service.isChecked = switch
|
||||
binding.switchStartService.isChecked = switch
|
||||
val pos = lstGuid.indexOf(guid.toString())
|
||||
if (pos >= 0) {
|
||||
listview?.setItemChecked(pos, true)
|
||||
@@ -70,17 +78,15 @@ class TaskerActivity : BaseActivity() {
|
||||
}
|
||||
|
||||
val extraBundle = Bundle()
|
||||
extraBundle.putBoolean(AppConfig.TASKER_EXTRA_BUNDLE_SWITCH, switch_start_service.isChecked)
|
||||
extraBundle.putBoolean(AppConfig.TASKER_EXTRA_BUNDLE_SWITCH, binding.switchStartService.isChecked)
|
||||
extraBundle.putString(AppConfig.TASKER_EXTRA_BUNDLE_GUID, lstGuid[position])
|
||||
val intent = Intent()
|
||||
|
||||
val remarks = lstData[position]
|
||||
var blurb = ""
|
||||
|
||||
if (switch_start_service.isChecked) {
|
||||
blurb = "Start $remarks"
|
||||
val blurb = if (binding.switchStartService.isChecked) {
|
||||
"Start $remarks"
|
||||
} else {
|
||||
blurb = "Stop $remarks"
|
||||
"Stop $remarks"
|
||||
}
|
||||
|
||||
intent.putExtra(AppConfig.TASKER_EXTRA_BUNDLE, extraBundle)
|
||||
@@ -89,9 +95,9 @@ class TaskerActivity : BaseActivity() {
|
||||
finish()
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
|
||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||
menuInflater.inflate(R.menu.action_server, menu)
|
||||
val del_config = menu?.findItem(R.id.del_config)
|
||||
val del_config = menu.findItem(R.id.del_config)
|
||||
del_config?.isVisible = false
|
||||
return super.onCreateOptionsMenu(menu)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
package com.v2ray.ang.ui
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import com.google.zxing.WriterException
|
||||
import com.v2ray.ang.R
|
||||
import com.v2ray.ang.databinding.ActivityLogcatBinding
|
||||
import com.v2ray.ang.extension.toast
|
||||
import com.v2ray.ang.util.AngConfigManager
|
||||
|
||||
class UrlSchemeActivity : BaseActivity() {
|
||||
private lateinit var binding: ActivityLogcatBinding
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = ActivityLogcatBinding.inflate(layoutInflater)
|
||||
val view = binding.root
|
||||
setContentView(view)
|
||||
|
||||
var shareUrl: String = ""
|
||||
try {
|
||||
intent?.apply {
|
||||
when (action) {
|
||||
Intent.ACTION_SEND -> {
|
||||
if ("text/plain" == type) {
|
||||
intent.getStringExtra(Intent.EXTRA_TEXT)?.let {
|
||||
shareUrl = it
|
||||
}
|
||||
}
|
||||
}
|
||||
Intent.ACTION_VIEW -> {
|
||||
val uri: Uri? = intent.data
|
||||
shareUrl = uri?.getQueryParameter("url")!!
|
||||
}
|
||||
}
|
||||
}
|
||||
toast(shareUrl)
|
||||
val count = AngConfigManager.importBatchConfig(shareUrl, "", false)
|
||||
if (count > 0) {
|
||||
toast(R.string.toast_success)
|
||||
} else {
|
||||
toast(R.string.toast_failure)
|
||||
}
|
||||
startActivity(Intent(this, MainActivity::class.java))
|
||||
finish()
|
||||
} catch (e: WriterException) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,216 @@
|
||||
package com.v2ray.ang.ui
|
||||
|
||||
import android.Manifest
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.provider.OpenableColumns
|
||||
import android.util.Log
|
||||
import android.view.*
|
||||
import android.view.View.GONE
|
||||
import android.view.View.VISIBLE
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.tbruyelle.rxpermissions.RxPermissions
|
||||
import com.tencent.mmkv.MMKV
|
||||
import com.v2ray.ang.AppConfig
|
||||
import com.v2ray.ang.R
|
||||
import com.v2ray.ang.databinding.ActivitySubSettingBinding
|
||||
import com.v2ray.ang.databinding.ItemRecyclerUserAssetBinding
|
||||
import com.v2ray.ang.extension.toTrafficString
|
||||
import com.v2ray.ang.extension.toast
|
||||
import com.v2ray.ang.util.MmkvManager
|
||||
import com.v2ray.ang.util.Utils
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.net.HttpURLConnection
|
||||
import java.net.InetSocketAddress
|
||||
import java.net.Proxy
|
||||
import java.net.URL
|
||||
import java.text.DateFormat
|
||||
import java.util.*
|
||||
|
||||
class UserAssetActivity : BaseActivity() {
|
||||
private lateinit var binding: ActivitySubSettingBinding
|
||||
private val settingsStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_SETTING, MMKV.MULTI_PROCESS_MODE) }
|
||||
|
||||
val extDir by lazy { File(Utils.userAssetPath(this)) }
|
||||
val geofiles = arrayOf("geosite.dat", "geoip.dat")
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = ActivitySubSettingBinding.inflate(layoutInflater)
|
||||
val view = binding.root
|
||||
setContentView(view)
|
||||
title = getString(R.string.title_user_asset_setting)
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
|
||||
binding.recyclerView.setHasFixedSize(true)
|
||||
binding.recyclerView.layoutManager = LinearLayoutManager(this)
|
||||
binding.recyclerView.adapter = UserAssetAdapter()
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||
menuInflater.inflate(R.menu.menu_asset, menu)
|
||||
return super.onCreateOptionsMenu(menu)
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
|
||||
R.id.add_file -> {
|
||||
showFileChooser()
|
||||
true
|
||||
}
|
||||
|
||||
R.id.download_file -> {
|
||||
downloadGeoFiles()
|
||||
true
|
||||
}
|
||||
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
private fun showFileChooser() {
|
||||
RxPermissions(this).request(Manifest.permission.READ_EXTERNAL_STORAGE).subscribe {
|
||||
if (it) {
|
||||
val intent = Intent(Intent.ACTION_GET_CONTENT)
|
||||
intent.type = "*/*"
|
||||
intent.addCategory(Intent.CATEGORY_OPENABLE)
|
||||
|
||||
try {
|
||||
chooseFile.launch(
|
||||
Intent.createChooser(
|
||||
intent,
|
||||
getString(R.string.title_file_chooser)
|
||||
)
|
||||
)
|
||||
} catch (ex: android.content.ActivityNotFoundException) {
|
||||
toast(R.string.toast_require_file_manager)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val chooseFile =
|
||||
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||
val uri = it.data?.data
|
||||
if (it.resultCode == RESULT_OK && uri != null) {
|
||||
try {
|
||||
copyFile(uri)
|
||||
} catch (e: Exception) {
|
||||
toast(R.string.toast_asset_copy_failed)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun copyFile(uri: Uri): String {
|
||||
val targetFile = File(extDir, getCursorName(uri) ?: uri.toString())
|
||||
contentResolver.openInputStream(uri).use { inputStream ->
|
||||
targetFile.outputStream().use { fileOut ->
|
||||
inputStream?.copyTo(fileOut)
|
||||
toast(R.string.toast_success)
|
||||
binding.recyclerView.adapter?.notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
return targetFile.path
|
||||
}
|
||||
|
||||
private fun getCursorName(uri: Uri): String? = try {
|
||||
contentResolver.query(uri, null, null, null, null)?.let { cursor ->
|
||||
cursor.run {
|
||||
if (moveToFirst()) getString(getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME))
|
||||
else null
|
||||
}.also { cursor.close() }
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
null
|
||||
}
|
||||
|
||||
private fun downloadGeoFiles() {
|
||||
val httpPort = Utils.parseInt(settingsStorage?.decodeString(AppConfig.PREF_HTTP_PORT), AppConfig.PORT_HTTP.toInt())
|
||||
|
||||
toast(R.string.msg_downloading_content)
|
||||
geofiles.forEach {
|
||||
//toast(getString(R.string.msg_downloading_content) + it)
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
val result = downloadGeo(it, 60000, httpPort)
|
||||
launch(Dispatchers.Main) {
|
||||
if (result) {
|
||||
toast(getString(R.string.toast_success) + " " + it)
|
||||
binding.recyclerView.adapter?.notifyDataSetChanged()
|
||||
} else {
|
||||
toast(getString(R.string.toast_failure) + " " + it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun downloadGeo(name: String, timeout: Int, httpPort: Int): Boolean {
|
||||
val url = AppConfig.geoUrl + name
|
||||
val targetTemp = File(extDir, name + "_temp")
|
||||
val target = File(extDir, name)
|
||||
var conn: HttpURLConnection? = null
|
||||
//Log.d(AppConfig.ANG_PACKAGE, url)
|
||||
|
||||
try {
|
||||
conn = URL(url).openConnection(
|
||||
Proxy(
|
||||
Proxy.Type.HTTP,
|
||||
InetSocketAddress("127.0.0.1", httpPort)
|
||||
)
|
||||
) as HttpURLConnection
|
||||
conn.connectTimeout = timeout
|
||||
conn.readTimeout = timeout
|
||||
val inputStream = conn.inputStream
|
||||
val responseCode = conn.responseCode
|
||||
if (responseCode == HttpURLConnection.HTTP_OK) {
|
||||
FileOutputStream(targetTemp).use { output ->
|
||||
inputStream.copyTo(output)
|
||||
}
|
||||
|
||||
targetTemp.renameTo(target)
|
||||
}
|
||||
return true
|
||||
} catch (e: Exception) {
|
||||
Log.e(AppConfig.ANG_PACKAGE, Log.getStackTraceString(e))
|
||||
return false
|
||||
} finally {
|
||||
conn?.disconnect()
|
||||
}
|
||||
}
|
||||
|
||||
inner class UserAssetAdapter : RecyclerView.Adapter<UserAssetViewHolder>() {
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UserAssetViewHolder {
|
||||
return UserAssetViewHolder(ItemRecyclerUserAssetBinding.inflate(LayoutInflater.from(parent.context), parent, false))
|
||||
}
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
override fun onBindViewHolder(holder: UserAssetViewHolder, position: Int) {
|
||||
val file = extDir.listFiles()?.getOrNull(position) ?: return
|
||||
holder.itemUserAssetBinding.assetName.text = file.name
|
||||
val dateFormat = DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.MEDIUM)
|
||||
holder.itemUserAssetBinding.assetProperties.text = "${file.length().toTrafficString()} • ${dateFormat.format(Date(file.lastModified()))}"
|
||||
if (file.name in geofiles) {
|
||||
holder.itemUserAssetBinding.layoutRemove.visibility = GONE
|
||||
} else {
|
||||
holder.itemUserAssetBinding.layoutRemove.visibility = VISIBLE
|
||||
}
|
||||
holder.itemUserAssetBinding.layoutRemove.setOnClickListener {
|
||||
file.delete()
|
||||
binding.recyclerView.adapter?.notifyItemRemoved(position)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
return extDir.listFiles()?.size ?: 0
|
||||
}
|
||||
}
|
||||
|
||||
class UserAssetViewHolder(val itemUserAssetBinding: ItemRecyclerUserAssetBinding) : RecyclerView.ViewHolder(itemUserAssetBinding.root)
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user