commit 2d8ed22dba436f8769a61dc6413d42b36891055f Author: 2dust <31833384+2dust@users.noreply.github.com> Date: Fri Oct 11 14:17:10 2019 +0800 mge diff --git a/.github/issue_template.md b/.github/issue_template.md new file mode 100644 index 000000000..71a6f01c7 --- /dev/null +++ b/.github/issue_template.md @@ -0,0 +1,28 @@ +在提出问题前请先自行排除服务器端问题,同时也请通过搜索确认是否有人提出过相同问题。 + + +### 预期行为 +描述你认为应该发生什么 + +### 实际行为 +描述实际发生了什么 + +### 复现方法 +1. +2. +3. + +### 日志信息 +
+通过 `adb logcat -s com.v2ray.ang GoLog V2rayConfigUtilGoLog Main` 获取日志。请自行删减日志中可能出现的敏感信息。 + +如果问题可重现,建议先执行`adb logcat -c`清空系统日志再执行上述命令,再操作重现问题。 +``` +在这里粘贴日志 +``` +
+ +### 环境信息 + +### 额外信息(可选) + diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..ca9dfff26 --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +V2rayNG/app/src/main/res/layout/activity_inapp_buy.xml +V2rayNG/app/src/main/assets/geoip.dat +V2rayNG/app/src/main/assets/geosite.dat +V2rayNG/app/src/main/java/com/v2ray/ang/InappBuyActivity.java +V2rayNG/gradle/wrapper/gradle-wrapper.properties +V2rayNG/gradle/wrapper/gradle-wrapper.properties +*.dat +*.jks +V2rayNG/gradle/wrapper/gradle-wrapper.properties +V2rayNG/gradle/wrapper/gradle-wrapper.properties +V2rayNG/app/release/output.json \ No newline at end of file diff --git a/AndroidLibV2rayLite/.travis.yml b/AndroidLibV2rayLite/.travis.yml new file mode 100644 index 000000000..c3beb7db1 --- /dev/null +++ b/AndroidLibV2rayLite/.travis.yml @@ -0,0 +1,26 @@ +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 diff --git a/AndroidLibV2rayLite/CoreI/Status.go b/AndroidLibV2rayLite/CoreI/Status.go new file mode 100644 index 000000000..a5d4b9423 --- /dev/null +++ b/AndroidLibV2rayLite/CoreI/Status.go @@ -0,0 +1,63 @@ +package CoreI + +import ( + v2core "v2ray.com/core" +) + +type Status struct { + IsRunning bool + PackageName string + + Vpoint v2core.Server +} + +func CheckVersion() int { + return 20 +} + +func (v *Status) GetDataDir() string { + return v.PackageName +} + +func (v *Status) GetApp(name string) string { + return v.PackageName + name +} + +func (v *Status) GetTun2socksArgs(localDNS bool, enableIPv6 bool) (ret []string) { + ret = []string{"--netif-ipaddr", + "26.26.26.2", + "--netif-netmask", + "255.255.255.252", + "--socks-server-addr", + "127.0.0.1:10808", + "--tunmtu", + "1500", + "--loglevel", + "notice", + "--enable-udprelay", + "--sock-path", + v.GetDataDir() + "sock_path", + } + + if enableIPv6 { + ret = append(ret, "--netif-ip6addr", "da26:2626::2") + } + + if localDNS { + ret = append(ret, "--dnsgw", "127.0.0.1:10807") + } + + return +} + +func (v *Status) GetVPNSetupArg(localDNS bool, enableIPv6 bool) (ret string) { + ret = "m,1500 a,26.26.26.1,30 r,0.0.0.0,0" + + if enableIPv6 { + ret += " a,da26:2626::1,126 r,::,0" + } + if localDNS { + ret += " d,26.26.26.2" + } + return +} diff --git a/AndroidLibV2rayLite/Makefile b/AndroidLibV2rayLite/Makefile new file mode 100644 index 000000000..f4a4ec68b --- /dev/null +++ b/AndroidLibV2rayLite/Makefile @@ -0,0 +1,34 @@ +pb: + go get -u github.com/golang/protobuf/protoc-gen-go + @echo "pb Start" +asset: + bash gen_assets.sh download + mkdir assets + cp -v data/*.dat assets/ + # cd assets;curl https://raw.githubusercontent.com/2dust/AndroidLibV2rayLite/master/data/geosite.dat > geosite.dat + # cd assets;curl https://raw.githubusercontent.com/2dust/AndroidLibV2rayLite/master/data/geoip.dat > geoip.dat + +shippedBinary: + cd shippedBinarys; $(MAKE) shippedBinary + +fetchDep: + -go get github.com/2dust/AndroidLibV2rayLite + go get github.com/2dust/AndroidLibV2rayLite + +ANDROID_HOME=$(HOME)/android-sdk-linux +export ANDROID_HOME +PATH:=$(PATH):$(GOPATH)/bin +export PATH +downloadGoMobile: + go get golang.org/x/mobile/cmd/... + sudo apt-get install -qq libstdc++6:i386 lib32z1 expect + cd ~ ;curl -L https://raw.githubusercontent.com/2dust/AndroidLibV2rayLite/master/ubuntu-cli-install-android-sdk.sh | sudo bash - > /dev/null + ls ~ + ls ~/android-sdk-linux/ + gomobile init ;gomobile bind -v -tags json github.com/2dust/AndroidLibV2rayLite + +BuildMobile: + @echo Stub + +all: asset pb shippedBinary fetchDep + @echo DONE diff --git a/AndroidLibV2rayLite/Process/Escort/escort.go b/AndroidLibV2rayLite/Process/Escort/escort.go new file mode 100644 index 000000000..69c9430a8 --- /dev/null +++ b/AndroidLibV2rayLite/Process/Escort/escort.go @@ -0,0 +1,87 @@ +package Escort + +import ( + "os" + "os/exec" + "time" + + "log" + + "github.com/2dust/AndroidLibV2rayLite/CoreI" +) + +func (v *Escorting) EscortRun(proc string, pt []string, additionalEnv string, sendFd func() int) { + log.Println(proc, pt) + count := 0 + for count <= 42 { + cmd := exec.Command(proc, pt...) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + if len(additionalEnv) > 0 { + //additionalEnv := "FOO=bar" + newEnv := append(os.Environ(), additionalEnv) + cmd.Env = newEnv + } + + if err := cmd.Start(); err != nil { + log.Println("EscortRun cmd.Start err", err) + goto CMDERROR + } + + if v.escortProcess == nil { + log.Println("EscortRun v.escortProcess nil") + break + } + + *v.escortProcess = append(*v.escortProcess, cmd.Process) + log.Println("EscortRun Waiting....") + + if count > 0 { + go func() { + time.Sleep(time.Second) + sendFd() + }() + } + + if err := cmd.Wait(); err != nil { + log.Println("EscortRun cmd.Wait err:", err) + } + + CMDERROR: + if v.Status.IsRunning { + log.Println("EscortRun Unexpected Exit, Restart now.") + count++ + } else { + log.Println("EscortRun Exit") + break + } + } +} + +func (v *Escorting) EscortingUp() { + if v.escortProcess != nil { + return + } + v.escortProcess = new([](*os.Process)) +} + +func (v *Escorting) EscortingDown() { + if v.escortProcess == nil { + return + } + + log.Println("EscortingDown() Killing all escorted process ") + for _, pr := range *v.escortProcess { + pr.Kill() + if _, err := pr.Wait(); err != nil { + log.Println("EscortingDown pr.Wait err:", err) + } + } + v.escortProcess = nil +} + +type Escorting struct { + escortProcess *[](*os.Process) + Status *CoreI.Status +} diff --git a/AndroidLibV2rayLite/README.md b/AndroidLibV2rayLite/README.md new file mode 100644 index 000000000..1c1962bc2 --- /dev/null +++ b/AndroidLibV2rayLite/README.md @@ -0,0 +1 @@ +# AndroidLibV2rayLite diff --git a/AndroidLibV2rayLite/VPN/vpnservice_support.go b/AndroidLibV2rayLite/VPN/vpnservice_support.go new file mode 100644 index 000000000..f7456cace --- /dev/null +++ b/AndroidLibV2rayLite/VPN/vpnservice_support.go @@ -0,0 +1,279 @@ +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 +} diff --git a/AndroidLibV2rayLite/VPN/vpnservice_support_test.go b/AndroidLibV2rayLite/VPN/vpnservice_support_test.go new file mode 100644 index 000000000..883f2f6e9 --- /dev/null +++ b/AndroidLibV2rayLite/VPN/vpnservice_support_test.go @@ -0,0 +1,151 @@ +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()) + }) + } +} diff --git a/AndroidLibV2rayLite/compile-tun2socks.sh b/AndroidLibV2rayLite/compile-tun2socks.sh new file mode 100644 index 000000000..ef0fdbe68 --- /dev/null +++ b/AndroidLibV2rayLite/compile-tun2socks.sh @@ -0,0 +1,47 @@ +#!/bin/bash + +set -o errexit +set -o pipefail +set -o nounset + +# Set magic variables for current file & dir +__dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +__file="${__dir}/$(basename "${BASH_SOURCE[0]}")" +__base="$(basename ${__file} .sh)" + +if [[ ! -d $NDK_HOME ]]; then + echo "Android NDK: NDK_HOME not found. please set env \$NDK_HOME" + exit 1 +fi + +TMPDIR=$(mktemp -d) +clear_tmp () { + rm -rf $TMPDIR +} + +trap 'echo -e "Aborted, error $? in command: $BASH_COMMAND"; trap ERR; clear_tmp; exit 1' ERR INT +install -m644 $__dir/tun2socks.mk $TMPDIR/ + +pushd $TMPDIR +git clone --depth=1 https://github.com/shadowsocks/badvpn.git +git clone --depth=1 https://github.com/shadowsocks/libancillary.git +$NDK_HOME/ndk-build \ + NDK_PROJECT_PATH=. \ + APP_BUILD_SCRIPT=./tun2socks.mk \ + APP_ABI=all \ + APP_PLATFORM=android-19 \ + NDK_LIBS_OUT=$TMPDIR/libs \ + NDK_OUT=$TMPDIR/tmp \ + APP_SHORT_COMMANDS=false LOCAL_SHORT_COMMANDS=false -B -j4 + +install -v -m755 libs/armeabi-v7a/tun2socks $__dir/shippedBinarys/ArchDep/arm/ +install -v -m755 libs/arm64-v8a/tun2socks $__dir/shippedBinarys/ArchDep/arm64/ +install -v -m755 libs/x86/tun2socks $__dir/shippedBinarys/ArchDep/386/ +install -v -m755 libs/x86_64/tun2socks $__dir/shippedBinarys/ArchDep/amd64/ +popd + +pushd $__dir/shippedBinarys +make clean && make shippedBinary +popd + +rm -rf $TMPDIR \ No newline at end of file diff --git a/AndroidLibV2rayLite/gen_assets.sh b/AndroidLibV2rayLite/gen_assets.sh new file mode 100644 index 000000000..3a102fa2d --- /dev/null +++ b/AndroidLibV2rayLite/gen_assets.sh @@ -0,0 +1,81 @@ +#!/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 diff --git a/AndroidLibV2rayLite/interact.go b/AndroidLibV2rayLite/interact.go new file mode 100644 index 000000000..b15df2dce --- /dev/null +++ b/AndroidLibV2rayLite/interact.go @@ -0,0 +1,255 @@ +package libv2ray + +import ( + "fmt" + "io" + "log" + "os" + "strings" + "sync" + + "github.com/2dust/AndroidLibV2rayLite/CoreI" + "github.com/2dust/AndroidLibV2rayLite/Process/Escort" + "github.com/2dust/AndroidLibV2rayLite/VPN" + "github.com/2dust/AndroidLibV2rayLite/shippedBinarys" + mobasset "golang.org/x/mobile/asset" + + v2core "v2ray.com/core" + v2filesystem "v2ray.com/core/common/platform/filesystem" + v2stats "v2ray.com/core/features/stats" + v2serial "v2ray.com/core/infra/conf/serial" + _ "v2ray.com/core/main/distro/all" + v2internet "v2ray.com/core/transport/internet" + + v2applog "v2ray.com/core/app/log" + v2commlog "v2ray.com/core/common/log" +) + +const ( + v2Assert = "v2ray.location.asset" + assetperfix = "/dev/libv2rayfs0/asset" +) + +/*V2RayPoint V2Ray Point Server +This is territory of Go, so no getter and setters! +*/ +type V2RayPoint struct { + SupportSet V2RayVPNServiceSupportsSet + statsManager v2stats.Manager + + dialer *VPN.ProtectedDialer + status *CoreI.Status + escorter *Escort.Escorting + v2rayOP *sync.Mutex + closeChan chan struct{} + + PackageName string + DomainName string + ConfigureFileContent string + EnableLocalDNS bool + ForwardIpv6 bool +} + +/*V2RayVPNServiceSupportsSet To support Android VPN mode*/ +type V2RayVPNServiceSupportsSet interface { + Setup(Conf string) int + Prepare() int + Shutdown() int + Protect(int) int + OnEmitStatus(int, string) int + SendFd() int +} + +/*RunLoop Run V2Ray main loop + */ +func (v *V2RayPoint) RunLoop() (err error) { + v.v2rayOP.Lock() + defer v.v2rayOP.Unlock() + //Construct Context + v.status.PackageName = v.PackageName + + if !v.status.IsRunning { + v.closeChan = make(chan struct{}) + v.dialer.PrepareResolveChan() + go v.dialer.PrepareDomain(v.DomainName, v.closeChan) + go func() { + select { + // wait until resolved + case <-v.dialer.ResolveChan(): + // shutdown VPNService if server name can not reolved + if !v.dialer.IsVServerReady() { + log.Println("vServer cannot resolved, shutdown") + v.StopLoop() + v.SupportSet.Shutdown() + } + + // stop waiting if manually closed + case <-v.closeChan: + } + }() + + err = v.pointloop() + } + return +} + +/*StopLoop Stop V2Ray main loop + */ +func (v *V2RayPoint) StopLoop() (err error) { + v.v2rayOP.Lock() + defer v.v2rayOP.Unlock() + if v.status.IsRunning { + close(v.closeChan) + v.shutdownInit() + v.SupportSet.OnEmitStatus(0, "Closed") + } + return +} + +//Delegate Funcation +func (v *V2RayPoint) GetIsRunning() bool { + return v.status.IsRunning +} + +//Delegate Funcation +func (v V2RayPoint) QueryStats(tag string, direct string) int64 { + if v.statsManager == nil { + return 0 + } + counter := v.statsManager.GetCounter(fmt.Sprintf("inbound>>>%s>>>traffic>>>%s", tag, direct)) + if counter == nil { + return 0 + } + return counter.Set(0) +} + +func (v *V2RayPoint) shutdownInit() { + v.status.IsRunning = false + v.status.Vpoint.Close() + v.status.Vpoint = nil + v.statsManager = nil + v.escorter.EscortingDown() +} + +func (v *V2RayPoint) pointloop() error { + if err := v.runTun2socks(); err != nil { + log.Println(err) + return err + } + + log.Printf("EnableLocalDNS: %v\nForwardIpv6: %v\nDomainName: %s", + v.EnableLocalDNS, + v.ForwardIpv6, + v.DomainName) + + log.Println("loading v2ray config") + config, err := v2serial.LoadJSONConfig(strings.NewReader(v.ConfigureFileContent)) + if err != nil { + log.Println(err) + return err + } + + log.Println("new v2ray core") + inst, err := v2core.New(config) + if err != nil { + log.Println(err) + return err + } + v.status.Vpoint = inst + v.statsManager = inst.GetFeature(v2stats.ManagerType()).(v2stats.Manager) + + log.Println("start v2ray core") + v.status.IsRunning = true + if err := v.status.Vpoint.Start(); err != nil { + v.status.IsRunning = false + log.Println(err) + return err + } + + v.SupportSet.Prepare() + v.SupportSet.Setup(v.status.GetVPNSetupArg(v.EnableLocalDNS, v.ForwardIpv6)) + v.SupportSet.OnEmitStatus(0, "Running") + return nil +} + +func initV2Env() { + if os.Getenv(v2Assert) != "" { + return + } + //Initialize asset API, Since Raymond Will not let notify the asset location inside Process, + //We need to set location outside V2Ray + os.Setenv(v2Assert, assetperfix) + //Now we handle read + v2filesystem.NewFileReader = func(path string) (io.ReadCloser, error) { + if strings.HasPrefix(path, assetperfix) { + p := path[len(assetperfix)+1:] + //is it overridden? + //by, ok := overridedAssets[p] + //if ok { + // return os.Open(by) + //} + return mobasset.Open(p) + } + return os.Open(path) + } +} + +//Delegate Funcation +func TestConfig(ConfigureFileContent string) error { + initV2Env() + _, err := v2serial.LoadJSONConfig(strings.NewReader(ConfigureFileContent)) + return err +} + +/*NewV2RayPoint new V2RayPoint*/ +func NewV2RayPoint(s V2RayVPNServiceSupportsSet) *V2RayPoint { + initV2Env() + + // inject our own log writer + v2applog.RegisterHandlerCreator(v2applog.LogType_Console, + func(lt v2applog.LogType, + options v2applog.HandlerCreatorOptions) (v2commlog.Handler, error) { + return v2commlog.NewLogger(createStdoutLogWriter()), nil + }) + + dialer := VPN.NewPreotectedDialer(s) + v2internet.UseAlternativeSystemDialer(dialer) + status := &CoreI.Status{} + return &V2RayPoint{ + SupportSet: s, + v2rayOP: new(sync.Mutex), + status: status, + dialer: dialer, + escorter: &Escort.Escorting{Status: status}, + } +} + +func (v V2RayPoint) runTun2socks() error { + shipb := shippedBinarys.FirstRun{Status: v.status} + if err := shipb.CheckAndExport(); err != nil { + log.Println(err) + return err + } + + v.escorter.EscortingUp() + go v.escorter.EscortRun( + v.status.GetApp("tun2socks"), + v.status.GetTun2socksArgs(v.EnableLocalDNS, v.ForwardIpv6), "", + v.SupportSet.SendFd) + + return nil +} + +/*CheckVersion int +This func will return libv2ray binding version. +*/ +func CheckVersion() int { + return CoreI.CheckVersion() +} + +/*CheckVersionX string +This func will return libv2ray binding version and V2Ray version used. +*/ +func CheckVersionX() string { + return fmt.Sprintf("Libv2rayLite V%d, Core V%s", CheckVersion(), v2core.Version()) +} diff --git a/AndroidLibV2rayLite/libv2ray.go b/AndroidLibV2rayLite/libv2ray.go new file mode 100644 index 000000000..8692373da --- /dev/null +++ b/AndroidLibV2rayLite/libv2ray.go @@ -0,0 +1,3 @@ +package libv2ray + +//go:generate make all diff --git a/AndroidLibV2rayLite/readme.txt b/AndroidLibV2rayLite/readme.txt new file mode 100644 index 000000000..b2b2a781e --- /dev/null +++ b/AndroidLibV2rayLite/readme.txt @@ -0,0 +1 @@ +readme.txt diff --git a/AndroidLibV2rayLite/shippedBinarys/ArchDep/386/tun2socks b/AndroidLibV2rayLite/shippedBinarys/ArchDep/386/tun2socks new file mode 100644 index 000000000..f5c05b587 Binary files /dev/null and b/AndroidLibV2rayLite/shippedBinarys/ArchDep/386/tun2socks differ diff --git a/AndroidLibV2rayLite/shippedBinarys/ArchDep/amd64/tun2socks b/AndroidLibV2rayLite/shippedBinarys/ArchDep/amd64/tun2socks new file mode 100644 index 000000000..8f426499c Binary files /dev/null and b/AndroidLibV2rayLite/shippedBinarys/ArchDep/amd64/tun2socks differ diff --git a/AndroidLibV2rayLite/shippedBinarys/ArchDep/arm/tun2socks b/AndroidLibV2rayLite/shippedBinarys/ArchDep/arm/tun2socks new file mode 100644 index 000000000..b1aa74e35 Binary files /dev/null and b/AndroidLibV2rayLite/shippedBinarys/ArchDep/arm/tun2socks differ diff --git a/AndroidLibV2rayLite/shippedBinarys/ArchDep/arm64/tun2socks b/AndroidLibV2rayLite/shippedBinarys/ArchDep/arm64/tun2socks new file mode 100644 index 000000000..b46649ef2 Binary files /dev/null and b/AndroidLibV2rayLite/shippedBinarys/ArchDep/arm64/tun2socks differ diff --git a/AndroidLibV2rayLite/shippedBinarys/ArchIndep/holder b/AndroidLibV2rayLite/shippedBinarys/ArchIndep/holder new file mode 100644 index 000000000..e69de29bb diff --git a/AndroidLibV2rayLite/shippedBinarys/Makefile b/AndroidLibV2rayLite/shippedBinarys/Makefile new file mode 100644 index 000000000..f9c450e08 --- /dev/null +++ b/AndroidLibV2rayLite/shippedBinarys/Makefile @@ -0,0 +1,13 @@ +Platdep=shippedBinary.386 shippedBinary.amd64 shippedBinary.arm64 shippedBinary.arm + +shippedBinaryDep: + go get -u github.com/jteeuwen/go-bindata/... + +shippedBinary.%: + go-bindata -nometadata -nomemcopy -pkg shippedBinarys -o ./binary_$*.go -tags $* ArchIndep/ ArchDep/$*/ + +shippedBinary:shippedBinaryDep $(Platdep) + @echo "Done" + +clean: + -rm binary* diff --git a/AndroidLibV2rayLite/shippedBinarys/firstrun.go b/AndroidLibV2rayLite/shippedBinarys/firstrun.go new file mode 100644 index 000000000..fcbd8c7cc --- /dev/null +++ b/AndroidLibV2rayLite/shippedBinarys/firstrun.go @@ -0,0 +1,69 @@ +package shippedBinarys + +import ( + "log" + "os" + "strconv" + + "github.com/2dust/AndroidLibV2rayLite/CoreI" +) + +type FirstRun struct { + Status *CoreI.Status +} + +func (v *FirstRun) checkIfRcExist() error { + datadir := v.Status.GetDataDir() + if _, err := os.Stat(datadir + strconv.Itoa(CoreI.CheckVersion())); !os.IsNotExist(err) { + log.Println("file exists") + return nil + } + + + IndepDir, err := AssetDir("ArchIndep") + log.Println(IndepDir) + if err != nil { + return err + } + for _, fn := range IndepDir { + log.Println(datadir+"ArchIndep/"+fn) + + err := RestoreAsset(datadir, "ArchIndep/"+fn) + log.Println(err) + + //GrantPremission + os.Chmod(datadir+"ArchIndep/"+fn, 0700) + log.Println(os.Remove(datadir + fn)) + log.Println(os.Symlink(datadir+"ArchIndep/"+fn, datadir + fn)) + } + + + DepDir, err := AssetDir("ArchDep") + log.Println(DepDir) + if err != nil { + return err + } + for _, fn := range DepDir { + DepDir2, err := AssetDir("ArchDep/" + fn) + log.Println("ArchDep/" + fn) + if err != nil { + return err + } + for _, FND := range DepDir2 { + log.Println(datadir+"ArchDep/"+fn+"/"+FND) + + RestoreAsset(datadir, "ArchDep/"+fn+"/"+FND) + os.Chmod(datadir+"ArchDep/"+fn+"/"+FND, 0700) + log.Println(os.Remove(datadir + FND)) + log.Println(os.Symlink(datadir+"ArchDep/"+fn+"/"+FND, datadir+FND)) + } + } + s, _ := os.Create(datadir + strconv.Itoa(CoreI.CheckVersion())) + s.Close() + + return nil +} + +func (v *FirstRun) CheckAndExport() error { + return v.checkIfRcExist() +} diff --git a/AndroidLibV2rayLite/tun2socks.mk b/AndroidLibV2rayLite/tun2socks.mk new file mode 100644 index 000000000..99d84cb31 --- /dev/null +++ b/AndroidLibV2rayLite/tun2socks.mk @@ -0,0 +1,128 @@ +# 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 + diff --git a/AndroidLibV2rayLite/ubuntu-cli-install-android-sdk.sh b/AndroidLibV2rayLite/ubuntu-cli-install-android-sdk.sh new file mode 100644 index 000000000..126d74bae --- /dev/null +++ b/AndroidLibV2rayLite/ubuntu-cli-install-android-sdk.sh @@ -0,0 +1,79 @@ +#!/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 diff --git a/AndroidLibV2rayLite/v2logger.go b/AndroidLibV2rayLite/v2logger.go new file mode 100644 index 000000000..e78a5abd8 --- /dev/null +++ b/AndroidLibV2rayLite/v2logger.go @@ -0,0 +1,32 @@ +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)} + } +} diff --git a/CR.md b/CR.md new file mode 100644 index 000000000..0ec148b6e --- /dev/null +++ b/CR.md @@ -0,0 +1,29 @@ +v2rayNG 隐私条款 + +最后更新 2017-11-22 + +v2rayNG 尊重并保护所有用户的个人隐私权,为此我们向大众公开这份隐私条款。**您使用 v2rayNG 即代表您以阅读并同意了这份条款,如果您不同意这份条款请立即停止使用并卸载 v2rayNG。** + +1. 信息收集 + + v2rayNG 软件自身不会发送任何信息到开发者,但是您下载软件的应用市场(如 Google Play)可能会收集关于应用运行状态的相关信息并提供给 v2rayNG 开发者。有关这些信息,请阅读您使用的应用市场所提供的隐私条款。 + + v2rayNG 软件中可能包含需要通过 IAP 支付解锁的功能,您的支付信息将由相关的 IAP 渠道进行处理,而我们对支付信息没有访问权。 + + 当您向 v2rayNG 开发者反馈软件运行中的错误时,开发者可能会要求您提供软件以及系统的日志以帮助确认问题的原因。因日志中可能包括敏感信息,此类信息只能由您自己操作发送。**我们不对任何传输服务的安全性和隐私性做任何明示或暗示的担保,请您在传送相关信息时选择可以您自身可以接受的方式。** + +2. 信息共享 + + 我们不会向任何第三方出售收集到的用户数据。我们可能向外部开发者提供信息以协助软件的开发,但是在提供信息之前我们会传达相关保密义务并确定其可以遵守。 + +3. 信息存留 + + 除非有相关法律规定,我们会在 30 天内清除不需要继续使用的用户数据,或将统计数据整合为无法识别单个用户的综合报告。 + +4. 信息泄露 + + 我们会使用合理的技术和安全手段尽力保护用户的数据,但是无法保证数据的绝对安全。如果我们确认数据发生了泄露,我们会在 7 天内通过可用的渠道通知用户。**您同意不向我们追责任何因不可抗力而造成的损失。** + +5. 条款修改 + + 我们保留修改这份隐私条款的权利,但是会确保在更新条款前至少 30 天通过我们的可用渠道和应用内提示来通知用户。**在新条款生效后继续使用软件即表示您同意修改后的隐私条款。** diff --git a/LICENSE b/LICENSE new file mode 100644 index 000000000..94a9ed024 --- /dev/null +++ b/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/README.md b/README.md new file mode 100644 index 000000000..e710a5259 --- /dev/null +++ b/README.md @@ -0,0 +1,6 @@ +# v2rayNG + + +Get it on Google Play + + diff --git a/V2rayNG/.gitignore b/V2rayNG/.gitignore new file mode 100644 index 000000000..4d128e872 --- /dev/null +++ b/V2rayNG/.gitignore @@ -0,0 +1,9 @@ +*.iml +.gradle +/local.properties +/.idea +.DS_Store +/build +/captures +*.apk +signing.properties diff --git a/V2rayNG/app/.gitignore b/V2rayNG/app/.gitignore new file mode 100644 index 000000000..2abde4aab --- /dev/null +++ b/V2rayNG/app/.gitignore @@ -0,0 +1,2 @@ +/build +/google-services.json diff --git a/V2rayNG/app/build.gradle b/V2rayNG/app/build.gradle new file mode 100644 index 000000000..e025b84a8 --- /dev/null +++ b/V2rayNG/app/build.gradle @@ -0,0 +1,129 @@ +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' +apply plugin: 'kotlin-android-extensions' + +android { + compileSdkVersion 28 + buildToolsVersion '28.0.3' + + compileOptions { + targetCompatibility = "8" + sourceCompatibility = "8" + } + + defaultConfig { + applicationId "com.v2ray.ang" + minSdkVersion 17 + targetSdkVersion Integer.parseInt("$targetSdkVer") + multiDexEnabled true + versionCode 212 + versionName "1.0.2" + } + + signingConfigs { + release { + storeFile file("../key.jks") + keyAlias 'ang' + keyPassword '123456' + storePassword '123456' + } + debug { + storeFile file("../key.jks") + keyAlias 'ang' + keyPassword '123456' + storePassword '123456' + } + } + + buildTypes { + release { + minifyEnabled false + zipAlignEnabled false + shrinkResources false + signingConfig signingConfigs.release +// proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + debug { + minifyEnabled false + zipAlignEnabled false + shrinkResources false + signingConfig signingConfigs.release + } + } + + sourceSets { + main.java.srcDirs += 'src/main/kotlin' + } + + splits { + abi { + enable true + reset() + include 'x86', 'x86_64', 'armeabi-v7a', 'arm64-v8a' //select ABIs to build APKs for + universalApk true //generate an additional APK that contains all the ABIs + } + } + + // map for the version code + project.ext.versionCodes = ['armeabi-v7a': 1, 'arm64-v8a': 2, 'x86': 3, 'x86_64': 4] + + android.applicationVariants.all { variant -> + // assign different version code for each output + variant.outputs.each { output -> + output.versionCodeOverride = + project.ext.versionCodes.get(output.getFilter(com.android.build.OutputFile.ABI), 0) * + 1000000 + android.defaultConfig.versionCode + } + } +} + +dependencies { + implementation fileTree(include: ['*.jar'], dir: 'libs') + implementation 'com.android.support.constraint:constraint-layout:1.1.3' + testImplementation 'junit:junit:4.12' + implementation project(':dpreference') + implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion" + implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlinVersion" + // Android support library + implementation "com.android.support:support-v4:$supportLibVersion" + implementation "com.android.support:appcompat-v7:$supportLibVersion" + implementation "com.android.support:design:$supportLibVersion" + implementation "com.android.support:cardview-v7:$supportLibVersion" + implementation "com.android.support:preference-v7:$supportLibVersion" + implementation "com.android.support:recyclerview-v7:$supportLibVersion" + // DSL + implementation "org.jetbrains.anko:anko-sdk15:$ankoVersion" + implementation "org.jetbrains.anko:anko-support-v4:$ankoVersion" + implementation "org.jetbrains.anko:anko-appcompat-v7:$ankoVersion" + implementation "org.jetbrains.anko:anko-design:$ankoVersion" + implementation 'com.google.code.gson:gson:2.8.5' + implementation 'io.reactivex:rxjava:1.3.4' + implementation 'io.reactivex:rxandroid:1.2.1' + implementation 'com.tbruyelle.rxpermissions:rxpermissions:0.9.4@aar' + implementation 'com.dinuscxj:recycleritemdecoration:1.0.0' + implementation 'io.reactivex:rxkotlin:0.60.0' + implementation 'me.dm7.barcodescanner:core:1.9.8' + implementation 'me.dm7.barcodescanner:zxing:1.9.8' + implementation 'com.github.jorgecastilloprz:fabprogresscircle:1.01@aar' + implementation 'com.beust:klaxon:3.0.1' + implementation 'com.android.support:multidex:1.0.3' + + implementation(name: 'libv2ray', ext: 'aar') + //implementation(name: 'tun2socks', ext: 'aar') +} + +buildscript { + repositories { + google() + jcenter() + maven { url 'https://maven.google.com' } + } + dependencies { + classpath "org.jetbrains.kotlin:kotlin-android-extensions:$kotlinVersion" + } +} +repositories { + flatDir { + dirs 'libs' + } +} \ No newline at end of file diff --git a/V2rayNG/app/libs/Readme.md b/V2rayNG/app/libs/Readme.md new file mode 100644 index 000000000..0f2efdb3f --- /dev/null +++ b/V2rayNG/app/libs/Readme.md @@ -0,0 +1 @@ +https://github.com/2dust/v2rayNG/tree/master/AndroidLibV2rayLite diff --git a/V2rayNG/app/libs/libv2ray.aar b/V2rayNG/app/libs/libv2ray.aar new file mode 100644 index 000000000..9b0f93595 Binary files /dev/null and b/V2rayNG/app/libs/libv2ray.aar differ diff --git a/V2rayNG/app/proguard-rules.pro b/V2rayNG/app/proguard-rules.pro new file mode 100644 index 000000000..e56daed37 --- /dev/null +++ b/V2rayNG/app/proguard-rules.pro @@ -0,0 +1,61 @@ +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in G:\android-sdk/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the proguardFiles +# directive in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# Add any project specific keep options here: + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Gson uses generic type information stored in a class file when working with fields. Proguard +# removes such information by default, so configure it to keep all of it. +-keepattributes Signature + +# For using GSON @Expose annotation +-keepattributes *Annotation* + +# Gson specific classes +-keep class sun.misc.Unsafe { *; } + +-dontwarn org.apache.commons.** +-keep class org.apache.commons.** { *;} + +# Disable debug info output +-assumenosideeffects class android.util.Log { + public static boolean isLoggable(java.lang.String,int); + public static int v(...); + public static int i(...); + public static int w(...); + public static int d(...); + public static int e(...); +} +-assumenosideeffects class kotlin.jvm.internal.Intrinsics { + static void checkParameterIsNotNull(java.lang.Object, java.lang.String); + static void checkExpressionValueIsNotNull(java.lang.Object, java.lang.String); + static void throwUninitializedPropertyAccessException(java.lang.String); +} + +-dontwarn org.jetbrains.anko.internals.** +-keep class org.jetbrains.anko.internals.** { *;} + +-dontwarn rx.internal.util.unsafe.** +-keep class rx.internal.util.unsafe.** { *;} + +-dontwarn app.dinus.** +-keep class app.dinus.** { *;} + +-keepclassmembers class ** { + @com.hwangjr.rxbus.annotation.Subscribe public *; + @com.hwangjr.rxbus.annotation.Produce public *; +} + +-keep class libv2ray.** { *;} diff --git a/V2rayNG/app/src/androidTest/java/com/v2ray/ang/ApplicationTest.java b/V2rayNG/app/src/androidTest/java/com/v2ray/ang/ApplicationTest.java new file mode 100644 index 000000000..e221e7142 --- /dev/null +++ b/V2rayNG/app/src/androidTest/java/com/v2ray/ang/ApplicationTest.java @@ -0,0 +1,13 @@ +package com.v2ray.ang; + +import android.app.Application; +import android.test.ApplicationTestCase; + +/** + * Testing Fundamentals + */ +public class ApplicationTest extends ApplicationTestCase { + public ApplicationTest() { + super(Application.class); + } +} \ No newline at end of file diff --git a/V2rayNG/app/src/main/AndroidManifest.xml b/V2rayNG/app/src/main/AndroidManifest.xml new file mode 100644 index 000000000..3fe099409 --- /dev/null +++ b/V2rayNG/app/src/main/AndroidManifest.xml @@ -0,0 +1,125 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/V2rayNG/app/src/main/aidl/com/android/vending/billing/IInAppBillingService.aidl b/V2rayNG/app/src/main/aidl/com/android/vending/billing/IInAppBillingService.aidl new file mode 100644 index 000000000..2a492f784 --- /dev/null +++ b/V2rayNG/app/src/main/aidl/com/android/vending/billing/IInAppBillingService.aidl @@ -0,0 +1,144 @@ +/* + * 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); +} diff --git a/V2rayNG/app/src/main/assets/proxy_packagename.txt b/V2rayNG/app/src/main/assets/proxy_packagename.txt new file mode 100644 index 000000000..a60331611 --- /dev/null +++ b/V2rayNG/app/src/main/assets/proxy_packagename.txt @@ -0,0 +1,196 @@ +com.android.chrome +com.google.android.googlequicksearchbox +com.google.android.apps.photos +com.google.android.youtube +com.google.android.gm +com.google.android.apps.plus +com.android.vending +com.google.android.inputmethod.latin +com.google.android.apps.paidtasks +com.google.android.keep +com.google.android.gms.setup +com. google.android. apps.magazines +com.google.android.videos +com. google.android.gms +com.google.android.apps.books +com.google.android.music +com.google.android.play.games +com.google.android.gsf +com.google.android.gsf.login +com.app.pornhub +com.spotify.music +org.thunderdog.challegram +com.tumblr +com.twitter.android +com.xda.labs +com.kapp.youtube.final +com.google.android.ims +com.wire +mark.via.gp +com.downloader.video.tumblr +com.sololearn +com.cygames.shadowverse +com.felixfilip.scpae +amanita_design.samorost3.gp +com.devolver.reigns2 +com.utopia.pxview +ch.protonmail.android +com.perol.asdpl.pixivez +com.pinterest +com.paypal.android.p2pmobile +com.arthurivanets.owly +com.rubenmayayo.reddit +com.rayark.cytus2 +com.rayark.pluto +com.rayark.implosion +com.fireproofstudios.theroom4 +com.netflix.mediaclient +com.instagram.android +com.google.android.apps.hangoutsdialer +com.google.android.talk +com.google.android.apps.plus +com.google.android.apps.pdfviewer +com.google.android.apps.magazines +com.google.android.apps.nbu.files +com.evernote +net.tsapps.appsales +com.google.android.apps.translate +com.google.ar.lens +com.google.android.apps.adm +com.google.android.apps.googleassistant +tw.com.gamer.android.activecenter +org.telegram.plus +com.brave.browser +com.breel.wallpapers18 +com.teslacoilsw.launcher +com.lastpass.lpandroid +org.kustom.widget +com.fooview.android.fooview +com.google.android.apps.docs +com.google.android.apps.maps +com.facebook.services +com.facebook.system +com.facebook.katana +com.nianticlabs.ingress.prime.qa +com.vanced.android.youtube +com.nianticproject.ingress +com.quoord.tapatalkpro.activity +org.mozilla.firefox +com.reddit.frontpage +com.google.android.apps.fitness +au.com.shiftyjelly.pocketcasts +com.google.android.gms +com.android.providers.telephony +com.resilio.sync +com.google.android.apps.googlevoice +com.discord +com.cradle.iitc_mobile +be.mygod.vpnhotspot +com.alphainventor.filemanager +com.android.providers.downloads +com.apkpure.aegon +com.ballistiq.artstation +com.bitly.app +com.chrome.canary +com.chrome.dev +com.devhd.feedly +com.dropbox.android +com.estrongs.android.pop +com.estrongs.android.pop.pro +com.fastaccess.github +com.firstrowria.pushnotificationtester +com.fvd.eversync +com.gianlu.aria2app +com.github.yeriomin.yalpstore +com.google.android.apps.docs.editors.sheets +com.google.android.instantapps.supervisor +com.google.android.ogyoutube +com.google.android.partnersetup +com.google.android.syncadapters.calendar +com.google.android.syncadapters.contacts +com.google.android.tts +com.hochan.coldsoup +com.ifttt.ifttt +com.imgur.mobile +com.innologica.inoreader +com.instapaper.android +com.jarvanh.vpntether +com.mediapods.tumbpods +com.mgoogle.android.gms +com.microsoft.office.powerpoint +com.mixplorer +com.msd.consumerchinese +com.msd.professionalchinese +com.mss2011c.sharehelper +com.newin.nplayer.pro +com.oasisfeng.island +com.orekie.search +com.popularapp.videodownloaderforinstagram +com.pushbullet.android +com.rhmsoft.edit +com.slack +com.tencent.huatuo +com.termux +com.thunkable.android.hritvik00.freenom +com.topjohnwu.magisk +com.u91porn +com.u9porn +com.vimeo.android.videoapp +com.wuxiangai.refactor +com.yandex.browser +com.z28j.feel +de.robv.android.xposed.installer +dk.tacit.android.foldersync.full +es.rafalense.telegram.themes +es.rafalense.themes +flipboard.app +github.tornaco.xposedmoduletest +io.va.exposed +jp.pxv.android +me.tshine.easymark +net.teeha.android.url_shortener +onion.fire +org.fdroid.fdroid +org.mozilla.fennec_aurora +org.schabi.newpipe +org.telegram.messenger +org.torproject.android +org.xbmc.kodi +pl.zdunex25.updater +videodownloader.downloadvideo.downloader +com.quora.android +com.lingodeer +org.wikipedia +com.ninegag.android.app +com.duolingo +com.patreon.android +com.valvesoftware.android.steam.communimunity +co.wanqu.android +jp.bokete.app.android +com.vkontakte.android +com.amazon.mshop.android.shopping +com.ubisoft.dance.justdance2015companion +com.gameloft.android.anmp.glofta8hm +com.gameloft.android.anmp.glofta9hm +com.binance.dev +com.asahi.tida.tablet +com.theinitium.news +com.driverbrowser +com.thomsonreuters.reuters +com.nytimes.cn +com.android.providers.downloads.ui +com.avmovie +bbc.mobile.news.ww +org.mozilla.focus +io.github.javiewer +com.sonelli.juicessh +con.medium.reader +com.microsoft.skydrive +com.valvesoftware.android.steam.community +com.nintendo.zara +org.torproject.torbrowser_alpha +tv.twitch.android.app +com.shanga.walli +com.whatsapp +com.wire +com.simplehabit.simplehabitapp \ No newline at end of file diff --git a/V2rayNG/app/src/main/assets/v2ray_config.json b/V2rayNG/app/src/main/assets/v2ray_config.json new file mode 100644 index 000000000..c88ca53cd --- /dev/null +++ b/V2rayNG/app/src/main/assets/v2ray_config.json @@ -0,0 +1,105 @@ +{ + "stats":{}, + "log": { + "loglevel": "warning" + }, + "policy":{ + "levels": { + "8": { + "handshake": 4, + "connIdle": 300, + "uplinkOnly": 1, + "downlinkOnly": 1 + } + }, + "system": { + "statsInboundUplink": true, + "statsInboundDownlink": true + } + }, + "inbounds": [{ + "tag": "socks", + "port": 10808, + "protocol": "socks", + "settings": { + "auth": "noauth", + "udp": true, + "userLevel": 8 + }, + "sniffing": { + "enabled": true, + "destOverride": [ + "http", + "tls" + ] + } + }, + { + "tag": "http", + "port": 10809, + "protocol": "http", + "settings": { + "userLevel": 8 + } + } +], + "outbounds": [{ + "tag": "proxy", + "protocol": "vmess", + "settings": { + "vnext": [ + { + "address": "v2ray.cool", + "port": 10086, + "users": [ + { + "id": "a3482e88-686a-4a58-8126-99c9df64b7bf", + "alterId": 64, + "security": "auto", + "level": 8 + } + ] + } + ], + "servers": [ + { + "address": "v2ray.cool", + "method": "chacha20", + "ota": false, + "password": "123456", + "port": 10086, + "level": 8 + } + ] + }, + "streamSettings": { + "network": "tcp" + }, + "mux": { + "enabled": false + } + }, + { + "protocol": "freedom", + "settings": {}, + "tag": "direct" + }, + { + "protocol": "blackhole", + "tag": "block", + "settings": { + "response": { + "type": "http" + } + } + } + ], + "routing": { + "domainStrategy": "IPIfNonMatch", + "rules": [] + }, + "dns": { + "hosts": {}, + "servers": [] + } +} diff --git a/V2rayNG/app/src/main/ic_launcher-web.png b/V2rayNG/app/src/main/ic_launcher-web.png new file mode 100644 index 000000000..f6fc3b8ce Binary files /dev/null and b/V2rayNG/app/src/main/ic_launcher-web.png differ diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/helper/ItemTouchHelperAdapter.java b/V2rayNG/app/src/main/java/com/v2ray/ang/helper/ItemTouchHelperAdapter.java new file mode 100644 index 000000000..4cf695f27 --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/helper/ItemTouchHelperAdapter.java @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2015 Paul Burke + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.v2ray.ang.helper; + +import android.support.v7.widget.RecyclerView; +import android.support.v7.widget.helper.ItemTouchHelper; + +/** + * Interface to listen for a move or dismissal event from a {@link ItemTouchHelper.Callback}. + * + * @author Paul Burke (ipaulpro) + */ +public interface ItemTouchHelperAdapter { + + /** + * Called when an item has been dragged far enough to trigger a move. This is called every time + * an item is shifted, and not at the end of a "drop" event.
+ *
+ * Implementations should call {@link RecyclerView.Adapter#notifyItemMoved(int, int)} after + * adjusting the underlying data to reflect this move. + * + * @param fromPosition The start position of the moved item. + * @param toPosition Then resolved position of the moved item. + * @return True if the item was moved to the new adapter position. + * + * @see RecyclerView#getAdapterPositionFor(RecyclerView.ViewHolder) + * @see RecyclerView.ViewHolder#getAdapterPosition() + */ + boolean onItemMove(int fromPosition, int toPosition); + + + /** + * Called when an item has been dismissed by a swipe.
+ *
+ * Implementations should call {@link RecyclerView.Adapter#notifyItemRemoved(int)} after + * adjusting the underlying data to reflect this removal. + * + * @param position The position of the item dismissed. + * + * @see RecyclerView#getAdapterPositionFor(RecyclerView.ViewHolder) + * @see RecyclerView.ViewHolder#getAdapterPosition() + */ + void onItemDismiss(int position); +} diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/helper/ItemTouchHelperViewHolder.java b/V2rayNG/app/src/main/java/com/v2ray/ang/helper/ItemTouchHelperViewHolder.java new file mode 100644 index 000000000..e20014de5 --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/helper/ItemTouchHelperViewHolder.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2015 Paul Burke + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.v2ray.ang.helper; + +import android.support.v7.widget.helper.ItemTouchHelper; + +/** + * Interface to notify an item ViewHolder of relevant callbacks from {@link + * ItemTouchHelper.Callback}. + * + * @author Paul Burke (ipaulpro) + */ +public interface ItemTouchHelperViewHolder { + + /** + * Called when the {@link ItemTouchHelper} first registers an item as being moved or swiped. + * Implementations should update the item view to indicate it's active state. + */ + void onItemSelected(); + + + /** + * Called when the {@link ItemTouchHelper} has completed the move or swipe, and the active item + * state should be cleared. + */ + void onItemClear(); +} diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/helper/OnStartDragListener.java b/V2rayNG/app/src/main/java/com/v2ray/ang/helper/OnStartDragListener.java new file mode 100644 index 000000000..163f94de0 --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/helper/OnStartDragListener.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2015 Paul Burke + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.v2ray.ang.helper; + +import android.support.v7.widget.RecyclerView; + +/** + * Listener for manual initiation of a drag. + */ +public interface OnStartDragListener { + + /** + * Called when a view is requesting a start of a drag. + * + * @param viewHolder The holder of the view to drag. + */ + void onStartDrag(RecyclerView.ViewHolder viewHolder); + +} diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/helper/SimpleItemTouchHelperCallback.java b/V2rayNG/app/src/main/java/com/v2ray/ang/helper/SimpleItemTouchHelperCallback.java new file mode 100644 index 000000000..2d281d5ff --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/helper/SimpleItemTouchHelperCallback.java @@ -0,0 +1,123 @@ +/* + * Copyright (C) 2015 Paul Burke + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.v2ray.ang.helper; + +import android.graphics.Canvas; +import android.support.v7.widget.GridLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.support.v7.widget.helper.ItemTouchHelper; + +/** + * An implementation of {@link ItemTouchHelper.Callback} that enables basic drag & drop and + * swipe-to-dismiss. Drag events are automatically started by an item long-press.
+ *
+ * Expects the RecyclerView.Adapter to listen for {@link + * ItemTouchHelperAdapter} callbacks and the RecyclerView.ViewHolder to implement + * {@link ItemTouchHelperViewHolder}. + * + * @author Paul Burke (ipaulpro) + */ +public class SimpleItemTouchHelperCallback extends ItemTouchHelper.Callback { + + public static final float ALPHA_FULL = 1.0f; + + private final ItemTouchHelperAdapter mAdapter; + + public SimpleItemTouchHelperCallback(ItemTouchHelperAdapter adapter) { + mAdapter = adapter; + } + + @Override + public boolean isLongPressDragEnabled() { + return true; + } + + @Override + public boolean isItemViewSwipeEnabled() { + return false; + } + + @Override + public int getMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) { + // Set movement flags based on the layout manager + if (recyclerView.getLayoutManager() instanceof GridLayoutManager) { + final int dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN | ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT; + final int swipeFlags = 0; + return makeMovementFlags(dragFlags, swipeFlags); + } else { + final int dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN; + final int swipeFlags = ItemTouchHelper.START | ItemTouchHelper.END; + return makeMovementFlags(dragFlags, swipeFlags); + } + } + + @Override + public boolean onMove(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()); + return true; + } + + @Override + public void onSwiped(RecyclerView.ViewHolder viewHolder, int i) { + // Notify the adapter of the dismissal + mAdapter.onItemDismiss(viewHolder.getAdapterPosition()); + } + + @Override + public void onChildDraw(Canvas c, RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, boolean isCurrentlyActive) { + if (actionState == ItemTouchHelper.ACTION_STATE_SWIPE) { + // Fade out the view as it is swiped out of the parent's bounds + final float alpha = ALPHA_FULL - Math.abs(dX) / (float) viewHolder.itemView.getWidth(); + viewHolder.itemView.setAlpha(alpha); + viewHolder.itemView.setTranslationX(dX); + } else { + super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive); + } + } + + @Override + public void onSelectedChanged(RecyclerView.ViewHolder viewHolder, int actionState) { + // We only want the active item to change + if (actionState != ItemTouchHelper.ACTION_STATE_IDLE) { + if (viewHolder instanceof ItemTouchHelperViewHolder) { + // Let the view holder know that this item is being moved or dragged + ItemTouchHelperViewHolder itemViewHolder = (ItemTouchHelperViewHolder) viewHolder; + itemViewHolder.onItemSelected(); + } + } + + super.onSelectedChanged(viewHolder, actionState); + } + + @Override + public void clearView(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) { + super.clearView(recyclerView, viewHolder); + + viewHolder.itemView.setAlpha(ALPHA_FULL); + + if (viewHolder instanceof ItemTouchHelperViewHolder) { + // Tell the view holder it's time to restore the idle state + ItemTouchHelperViewHolder itemViewHolder = (ItemTouchHelperViewHolder) viewHolder; + itemViewHolder.onItemClear(); + } + } +} diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/util/AssetsUtil.java b/V2rayNG/app/src/main/java/com/v2ray/ang/util/AssetsUtil.java new file mode 100644 index 000000000..38f17d9c4 --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/util/AssetsUtil.java @@ -0,0 +1,121 @@ +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); + } + } +} \ No newline at end of file diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/util/Base64.java b/V2rayNG/app/src/main/java/com/v2ray/ang/util/Base64.java new file mode 100644 index 000000000..eb69a0626 --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/util/Base64.java @@ -0,0 +1,570 @@ +// 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: + *

+ * 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 + * http://iharder.net/xmlizable + * periodically to check for updates or to contribute improvements. + *

+ * + * @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. + * + *

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 source + * and writes the resulting four Base64 bytes to destination. + * The source and destination arrays can be manipulated + * anywhere along their length by specifying + * srcOffset and destOffset. + * This method does not check to make sure your arrays + * are large enough to accommodate srcOffset + 3 for + * the source array or destOffset + 4 for + * the destination array. + * The actual number of significant bytes in your array is + * given by numSigBytes. + * + * @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 destination 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 source + * and writes the resulting bytes (up to three of them) + * to destination. + * The source and destination arrays can be manipulated + * anywhere along their length by specifying + * srcOffset and destOffset. + * This method does not check to make sure your arrays + * are large enough to accommodate srcOffset + 4 for + * the source array or destOffset + 3 for + * the destination 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; + } +} diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/util/Base64DecoderException.java b/V2rayNG/app/src/main/java/com/v2ray/ang/util/Base64DecoderException.java new file mode 100644 index 000000000..b113e43f8 --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/util/Base64DecoderException.java @@ -0,0 +1,32 @@ +// 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; +} diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/util/IabException.java b/V2rayNG/app/src/main/java/com/v2ray/ang/util/IabException.java new file mode 100644 index 000000000..e6320808d --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/util/IabException.java @@ -0,0 +1,43 @@ +/* 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; } +} \ No newline at end of file diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/util/IabHelper.java b/V2rayNG/app/src/main/java/com/v2ray/ang/util/IabHelper.java new file mode 100644 index 000000000..911d20dae --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/util/IabHelper.java @@ -0,0 +1,979 @@ +/* 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. + *

+ * 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. + *

+ * 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. + *

+ * 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. + *

+ * 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 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 moreItemSkus, + List 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 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 purchases, List 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 purchases = new ArrayList(); + 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 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 ownedSkus = ownedItems.getStringArrayList( + RESPONSE_INAPP_ITEM_LIST); + ArrayList purchaseDataList = ownedItems.getStringArrayList( + RESPONSE_INAPP_PURCHASE_DATA_LIST); + ArrayList 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 moreSkus) + throws RemoteException, JSONException { + logDebug("Querying SKU details."); + ArrayList skuList = new ArrayList(); + 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 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 purchases, + final OnConsumeFinishedListener singleListener, + final OnConsumeMultiFinishedListener multiListener) { + final Handler handler = new Handler(); + flagStartAsync("consume"); + (new Thread(new Runnable() { + public void run() { + final List results = new ArrayList(); + 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); + } +} diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/util/IabResult.java b/V2rayNG/app/src/main/java/com/v2ray/ang/util/IabResult.java new file mode 100644 index 000000000..0fbe5b582 --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/util/IabResult.java @@ -0,0 +1,45 @@ +/* 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(); } +} + diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/util/Inventory.java b/V2rayNG/app/src/main/java/com/v2ray/ang/util/Inventory.java new file mode 100644 index 000000000..ae13e74fb --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/util/Inventory.java @@ -0,0 +1,91 @@ +/* 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 mSkuMap = new HashMap(); + Map mPurchaseMap = new HashMap(); + + 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 getAllOwnedSkus() { + return new ArrayList(mPurchaseMap.keySet()); + } + + /** Returns a list of all owned product IDs of a given type */ + List getAllOwnedSkus(String itemType) { + List result = new ArrayList(); + for (Purchase p : mPurchaseMap.values()) { + if (p.getItemType().equals(itemType)) result.add(p.getSku()); + } + return result; + } + + /** Returns a list of all purchases. */ + List getAllPurchases() { + return new ArrayList(mPurchaseMap.values()); + } + + void addSkuDetails(SkuDetails d) { + mSkuMap.put(d.getSku(), d); + } + + void addPurchase(Purchase p) { + mPurchaseMap.put(p.getSku(), p); + } +} diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/util/LogRecorder.java b/V2rayNG/app/src/main/java/com/v2ray/ang/util/LogRecorder.java new file mode 100644 index 000000000..ad4d54ae0 --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/util/LogRecorder.java @@ -0,0 +1,540 @@ +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 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 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... + *

+ * Request alternate ring buffer, 'main', 'system', 'radio' + * or 'events'. Multiple -b parameters are allowed and the + * results are interleaved. + *

+ * 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. + *

+ * 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. + *

+ * 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; + } + } + +} \ No newline at end of file diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/util/Purchase.java b/V2rayNG/app/src/main/java/com/v2ray/ang/util/Purchase.java new file mode 100644 index 000000000..d5e59153e --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/util/Purchase.java @@ -0,0 +1,63 @@ +/* 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; } +} diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/util/QRCodeDecoder.java b/V2rayNG/app/src/main/java/com/v2ray/ang/util/QRCodeDecoder.java new file mode 100644 index 000000000..1a16ac3ec --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/util/QRCodeDecoder.java @@ -0,0 +1,116 @@ +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 HINTS = new EnumMap<>(DecodeHintType.class); + + static { + List 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; + } + } +} \ No newline at end of file diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/util/Security.java b/V2rayNG/app/src/main/java/com/v2ray/ang/util/Security.java new file mode 100644 index 000000000..50f02e3c4 --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/util/Security.java @@ -0,0 +1,119 @@ +/* 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; + } +} diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/util/SkuDetails.java b/V2rayNG/app/src/main/java/com/v2ray/ang/util/SkuDetails.java new file mode 100644 index 000000000..b15cd4728 --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/util/SkuDetails.java @@ -0,0 +1,58 @@ +/* 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; + } +} diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/AngApplication.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/AngApplication.kt new file mode 100644 index 000000000..e54812cd4 --- /dev/null +++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/AngApplication.kt @@ -0,0 +1,31 @@ +package com.v2ray.ang + +import android.app.Application +//import com.squareup.leakcanary.LeakCanary +import com.v2ray.ang.util.AngConfigManager +import me.dozen.dpreference.DPreference +import org.jetbrains.anko.defaultSharedPreferences + +class AngApplication : Application() { + companion object { + const val PREF_LAST_VERSION = "pref_last_version" + } + + var firstRun = false + private set + + val defaultDPreference by lazy { DPreference(this, packageName + "_preferences") } + + override fun onCreate() { + super.onCreate() + +// LeakCanary.install(this) + + firstRun = defaultSharedPreferences.getInt(PREF_LAST_VERSION, 0) != BuildConfig.VERSION_CODE + if (firstRun) + defaultSharedPreferences.edit().putInt(PREF_LAST_VERSION, BuildConfig.VERSION_CODE).apply() + + //Logger.init().logLevel(if (BuildConfig.DEBUG) LogLevel.FULL else LogLevel.NONE) + AngConfigManager.inject(this) + } +} diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/AppConfig.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/AppConfig.kt new file mode 100644 index 000000000..9f2278db6 --- /dev/null +++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/AppConfig.kt @@ -0,0 +1,61 @@ +package com.v2ray.ang + +/** + * + * App Config Const + */ +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_INAPP_BUY_IS_PREMIUM = "pref_inapp_buy_is_premium" + const val VMESS_PROTOCOL: String = "vmess://" + const val SS_PROTOCOL: String = "ss://" + const val SOCKS_PROTOCOL: String = "socks://" + const val BROADCAST_ACTION_SERVICE = "com.v2ray.ang.action.service" + const val BROADCAST_ACTION_ACTIVITY = "com.v2ray.ang.action.activity" + const val BROADCAST_ACTION_WIDGET_CLICK = "com.v2ray.ang.action.widget.click" + + const val TASKER_EXTRA_BUNDLE = "com.twofortyfouram.locale.intent.extra.BUNDLE" + const val TASKER_EXTRA_STRING_BLURB = "com.twofortyfouram.locale.intent.extra.BLURB" + const val TASKER_EXTRA_BUNDLE_SWITCH = "tasker_extra_bundle_switch" + const val TASKER_EXTRA_BUNDLE_GUID = "tasker_extra_bundle_guid" + const val TASKER_DEFAULT_GUID = "Default" + + const val 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" + + const val androidpackagenamelistUrl = "https://raw.githubusercontent.com/2dust/androidpackagenamelist/master/proxy.txt" + const val v2rayCustomRoutingListUrl = "https://raw.githubusercontent.com/2dust/v2rayCustomRoutingList/master/" + const val v2rayNGIssues = "https://github.com/2dust/v2rayNG/issues" + const val promotionUrl = "https://1.2345345.xyz/ads.html" + + const val DNS_AGENT = "1.1.1.1" + const val DNS_DIRECT = "223.5.5.5" + + const val MSG_REGISTER_CLIENT = 1 + const val MSG_STATE_RUNNING = 11 + const val MSG_STATE_NOT_RUNNING = 12 + const val MSG_UNREGISTER_CLIENT = 2 + const val MSG_STATE_START = 3 + const val MSG_STATE_START_SUCCESS = 31 + const val MSG_STATE_START_FAILURE = 32 + const val MSG_STATE_STOP = 4 + const val MSG_STATE_STOP_SUCCESS = 41 + const val MSG_STATE_RESTART = 5 + + object EConfigType { + val Vmess = 1 + val Custom = 2 + val Shadowsocks = 3 + val Socks = 4 + } + +} \ No newline at end of file diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/AngConfig.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/AngConfig.kt new file mode 100644 index 000000000..c51d78b65 --- /dev/null +++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/AngConfig.kt @@ -0,0 +1,28 @@ +package com.v2ray.ang.dto + +data class AngConfig( + var index: Int, + var vmess: ArrayList, + var subItem: ArrayList +) { + data class VmessBean(var guid: String = "123456", + var address: String = "v2ray.cool", + var port: Int = 10086, + var id: String = "a3482e88-686a-4a58-8126-99c9df64b7bf", + var alterId: Int = 64, + var security: String = "aes-128-cfb", + var network: String = "tcp", + var remarks: String = "def", + var headerType: String = "", + var requestHost: String = "", + var path: String = "", + var streamSecurity: String = "", + var configType: Int = 1, + var configVersion: Int = 1, + var testResult: String = "", + var subid: String = "") + + data class SubItemBean(var id: String = "", + var remarks: String = "", + var url: String = "") +} diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/AppInfo.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/AppInfo.kt new file mode 100644 index 000000000..f99655a85 --- /dev/null +++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/AppInfo.kt @@ -0,0 +1,9 @@ +package com.v2ray.ang.dto + +import android.graphics.drawable.Drawable + +data class AppInfo(val appName: String, + val packageName: String, + val appIcon: Drawable, + val isSystemApp: Boolean, + var isSelected: Int) \ No newline at end of file diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/V2rayConfig.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/V2rayConfig.kt new file mode 100644 index 000000000..ec1a87eca --- /dev/null +++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/V2rayConfig.kt @@ -0,0 +1,142 @@ +package com.v2ray.ang.dto + +data class V2rayConfig( + val stats: Any?=null, + val log: LogBean, + val policy: PolicyBean, + val inbounds: ArrayList, + var outbounds: ArrayList, + var dns: DnsBean, + val routing: RoutingBean) { + + data class LogBean(val access: String, + val error: String, + val loglevel: String) + + data class InboundBean( + var tag: String, + var port: Int, + var protocol: String, + var listen: String?=null, + val settings: InSettingsBean, + val sniffing: SniffingBean?) { + + data class InSettingsBean(val auth: String? = null, + val udp: Boolean? = null, + val userLevel: Int? =null, + val address: String? = null, + val port: Int? = null, + val network: String? = null) + + data class SniffingBean(var enabled: Boolean, + val destOverride: List) + } + + data class OutboundBean(val tag: String, + var protocol: String, + var settings: OutSettingsBean?, + var streamSettings: StreamSettingsBean?, + var mux: MuxBean?) { + + data class OutSettingsBean(var vnext: List?, + var servers: List?, + var response: Response) { + + data class VnextBean(var address: String, + var port: Int, + var users: List) { + + data class UsersBean(var id: String, + var alterId: Int, + var security: String, + var level: Int) + } + + data class ServersBean(var address: String, + var method: String, + var ota: Boolean, + var password: String, + var port: Int, + var level: Int) + + 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 TcpsettingsBean(var connectionReuse: Boolean = true, + var header: HeaderBean = HeaderBean()) { + data class HeaderBean(var type: String = "none", + var request: Any? = null, + var response: Any? = null) + } + + data class KcpsettingsBean(var mtu: Int = 1350, + var tti: Int = 20, + var uplinkCapacity: Int = 12, + var downlinkCapacity: Int = 100, + var congestion: Boolean = false, + var readBufferSize: Int = 1, + var writeBufferSize: Int = 1, + var header: HeaderBean = HeaderBean()) { + data class HeaderBean(var type: String = "none") + } + + data class WssettingsBean(var connectionReuse: Boolean = true, + var path: String = "", + var headers: HeadersBean = HeadersBean()) { + data class HeadersBean(var Host: String = "") + } + + data class HttpsettingsBean(var host: List = ArrayList(), var path: String = "") + + data class TlssettingsBean(var allowInsecure: Boolean = true, + var serverName: String = "") + + data class QuicsettingBean(var security: String = "none", + var key: String = "", + var header: HeaderBean = HeaderBean()) { + data class HeaderBean(var type: String = "none") + } + } + + data class MuxBean(var enabled: Boolean) + } + + //data class DnsBean(var servers: List) + data class DnsBean(var servers: List?=null, + var hosts: Map?=null + ) { + data class ServersBean(var address: String = "", + var port: Int = 0, + var domains: List?) + } + + data class RoutingBean(var domainStrategy: String, + var rules: ArrayList) { + + data class RulesBean(var type: String = "", + var ip: ArrayList? = null, + var domain: ArrayList? = null, + var outboundTag: String = "", + var port: String? = null, + var inboundTag: ArrayList? = null) + } + + data class PolicyBean(var levels: Map, + var system: Any?=null) { + data class LevelBean( + var handshake: Int? = null, + var connIdle: Int? = null, + var uplinkOnly: Int? = null, + var downlinkOnly: Int? = null) + } +} \ No newline at end of file diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/VmessQRCode.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/VmessQRCode.kt new file mode 100644 index 000000000..30618017b --- /dev/null +++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/VmessQRCode.kt @@ -0,0 +1,13 @@ +package com.v2ray.ang.dto + +data class VmessQRCode(var v: String = "", + var ps: String = "", + var add: String = "", + var port: String = "", + var id: String = "", + var aid: String = "", + var net: String = "", + var type: String = "", + var host: String = "", + var path: String = "", + var tls: String = "") \ No newline at end of file diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/extension/_Dialog.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/extension/_Dialog.kt new file mode 100644 index 000000000..f70b51301 --- /dev/null +++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/extension/_Dialog.kt @@ -0,0 +1,244 @@ +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, + onClick: (Int) -> Unit +): Unit = activity.selector(title, items, onClick) + +fun Context.selector( + title: CharSequence? = null, + items: List, + 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, f: (which: Int) -> Unit) { + items(items.toTypedArray(), f) + } + + fun items(items: Array, 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) + } +} \ No newline at end of file diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/extension/_Ext.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/extension/_Ext.kt new file mode 100644 index 000000000..fa028c47d --- /dev/null +++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/extension/_Ext.kt @@ -0,0 +1,64 @@ +package com.v2ray.ang.extension + +import android.content.Context +import android.os.Build +import com.v2ray.ang.AngApplication +import me.dozen.dpreference.DPreference +import org.json.JSONObject +import java.net.URLConnection + +/** + * Some extensions + */ + +val Context.v2RayApplication: AngApplication + get() = applicationContext as AngApplication + +val Context.defaultDPreference: DPreference + get() = v2RayApplication.defaultDPreference + + +fun JSONObject.putOpt(pair: Pair) = putOpt(pair.first, pair.second)!! +fun JSONObject.putOpt(pairs: Map) = pairs.forEach { putOpt(it.key to it.value) } + +const val threshold = 1000 +const val divisor = 1024F + +fun Long.toSpeedString() = toTrafficString() + "/s" + +fun Long.toTrafficString(): String { + if (this < threshold) + return "$this B" + + val kib = this / divisor + if (kib < threshold) + return "${kib.toShortString()} KB" + + val mib = kib / divisor + if (mib < threshold) + return "${mib.toShortString()} MB" + + val gib = mib / divisor + if (gib < threshold) + return "${gib.toShortString()} GB" + + val tib = gib / divisor + if (tib < threshold) + return "${tib.toShortString()} TB" + + val pib = tib / divisor + if (pib < threshold) + return "${pib.toShortString()} PB" + + return "∞" +} + +private fun Float.toShortString(): String { + val s = toString() + if (s.length <= 4) + return s + return s.substring(0, 4).removeSuffix(".") +} + +val URLConnection.responseLength: Long + get() = if (Build.VERSION.SDK_INT >= 24) contentLengthLong else contentLength.toLong() diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/extension/_Preference.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/extension/_Preference.kt new file mode 100644 index 000000000..dcfa46718 --- /dev/null +++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/extension/_Preference.kt @@ -0,0 +1,10 @@ +package com.v2ray.ang.extension + +import android.preference.Preference + +fun Preference.onClick(listener: () -> Unit) { + setOnPreferenceClickListener { + listener() + true + } +} \ No newline at end of file diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/receiver/TaskerReceiver.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/receiver/TaskerReceiver.kt new file mode 100644 index 000000000..dca1dc38e --- /dev/null +++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/receiver/TaskerReceiver.kt @@ -0,0 +1,35 @@ +package com.v2ray.ang.receiver + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.text.TextUtils +import com.google.zxing.WriterException +import com.v2ray.ang.AppConfig + +import com.v2ray.ang.util.Utils + +class TaskerReceiver : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent?) { + + try { + val bundle = intent?.getBundleExtra(AppConfig.TASKER_EXTRA_BUNDLE) + val switch = bundle?.getBoolean(AppConfig.TASKER_EXTRA_BUNDLE_SWITCH, false) + val guid = bundle?.getString(AppConfig.TASKER_EXTRA_BUNDLE_GUID, "") + + if (switch == null || guid == null || TextUtils.isEmpty(guid)) { + return + } else if (switch) { + if (guid == AppConfig.TASKER_DEFAULT_GUID) { + Utils.startVService(context) + } else { + Utils.startVService(context, guid) + } + } else { + Utils.stopVService(context) + } + } catch (e: WriterException) { + e.printStackTrace() + } + } +} diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/receiver/WidgetProvider.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/receiver/WidgetProvider.kt new file mode 100644 index 000000000..eddc21467 --- /dev/null +++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/receiver/WidgetProvider.kt @@ -0,0 +1,50 @@ +package com.v2ray.ang.receiver + +import android.app.PendingIntent +import android.appwidget.AppWidgetManager +import android.appwidget.AppWidgetProvider +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.widget.RemoteViews +import com.v2ray.ang.R +import com.v2ray.ang.AppConfig +import com.v2ray.ang.util.Utils +import org.jetbrains.anko.toast + +class WidgetProvider : AppWidgetProvider() { + /** + * 每次窗口小部件被更新都调用一次该方法 + */ + override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) { + super.onUpdate(context, appWidgetManager, appWidgetIds) + + val remoteViews = RemoteViews(context.packageName, R.layout.widget_switch) + val intent = Intent(AppConfig.BROADCAST_ACTION_WIDGET_CLICK) + val pendingIntent = PendingIntent.getBroadcast(context, R.id.layout_switch, intent, PendingIntent.FLAG_UPDATE_CURRENT) + remoteViews.setOnClickPendingIntent(R.id.layout_switch, pendingIntent) + + for (appWidgetId in appWidgetIds) { + appWidgetManager.updateAppWidget(appWidgetId, remoteViews) + } + } + + /** + * 接收窗口小部件点击时发送的广播 + */ + override fun onReceive(context: Context, intent: Intent) { + super.onReceive(context, intent) + if (AppConfig.BROADCAST_ACTION_WIDGET_CLICK == intent.action) { + + val isRunning = Utils.isServiceRun(context, "com.v2ray.ang.service.V2RayVpnService") + if (isRunning) { +// context.toast(R.string.toast_services_stop) + Utils.stopVService(context) + } else { +// context.toast(R.string.toast_services_start) + Utils.startVService(context) + } + } + } + +} diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/service/QSTileService.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/service/QSTileService.kt new file mode 100644 index 000000000..cbb824254 --- /dev/null +++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/service/QSTileService.kt @@ -0,0 +1,96 @@ +package com.v2ray.ang.service + +import android.annotation.TargetApi +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.graphics.drawable.Icon +import android.net.VpnService +import android.os.Build +import android.service.quicksettings.Tile +import android.service.quicksettings.TileService +import com.v2ray.ang.AppConfig +import com.v2ray.ang.R +import com.v2ray.ang.extension.defaultDPreference +import com.v2ray.ang.util.MessageUtil +import com.v2ray.ang.util.Utils +import org.jetbrains.anko.toast +import java.lang.ref.SoftReference + + +@TargetApi(Build.VERSION_CODES.N) +class QSTileService : TileService() { + + fun setState(state: Int) { + if (state == Tile.STATE_INACTIVE) { + qsTile?.state = Tile.STATE_INACTIVE + qsTile?.label = getString(R.string.app_name) + qsTile?.icon = Icon.createWithResource(applicationContext, R.drawable.ic_v_idle) + } else if (state == Tile.STATE_ACTIVE) { + qsTile?.state = Tile.STATE_ACTIVE + qsTile?.label = defaultDPreference.getPrefString(AppConfig.PREF_CURR_CONFIG_NAME, "NG") + qsTile?.icon = Icon.createWithResource(applicationContext, R.drawable.ic_v) + } + + + qsTile?.updateTile() + } + + override fun onStartListening() { + super.onStartListening() + setState(Tile.STATE_INACTIVE) + mMsgReceive = ReceiveMessageHandler(this) + registerReceiver(mMsgReceive, IntentFilter(AppConfig.BROADCAST_ACTION_ACTIVITY)) + MessageUtil.sendMsg2Service(this, AppConfig.MSG_REGISTER_CLIENT, "") + } + + override fun onStopListening() { + super.onStopListening() + + unregisterReceiver(mMsgReceive) + mMsgReceive = null + } + + override fun onClick() { + super.onClick() + when (qsTile.state) { + Tile.STATE_INACTIVE -> { + val intent = VpnService.prepare(this) + if (intent == null) + if (!Utils.startVService(this)) { + toast(R.string.app_tile_first_use) + } + } + Tile.STATE_ACTIVE -> { + Utils.stopVService(this) + } + } + } + + private var mMsgReceive: BroadcastReceiver? = null + + private class ReceiveMessageHandler(context: QSTileService) : BroadcastReceiver() { + internal var mReference: SoftReference = SoftReference(context) + override fun onReceive(ctx: Context?, intent: Intent?) { + val context = mReference.get() + when (intent?.getIntExtra("key", 0)) { + AppConfig.MSG_STATE_RUNNING -> { + context?.setState(Tile.STATE_ACTIVE) + } + AppConfig.MSG_STATE_NOT_RUNNING -> { + context?.setState(Tile.STATE_INACTIVE) + } + AppConfig.MSG_STATE_START_SUCCESS -> { + context?.setState(Tile.STATE_ACTIVE) + } + AppConfig.MSG_STATE_START_FAILURE -> { + context?.setState(Tile.STATE_INACTIVE) + } + AppConfig.MSG_STATE_STOP_SUCCESS -> { + context?.setState(Tile.STATE_INACTIVE) + } + } + } + } +} \ No newline at end of file diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/service/V2RayVpnService.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/service/V2RayVpnService.kt new file mode 100644 index 000000000..ada2cbc12 --- /dev/null +++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/service/V2RayVpnService.kt @@ -0,0 +1,492 @@ +package com.v2ray.ang.service + +import android.app.Notification +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.PendingIntent +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.content.pm.PackageManager +import android.graphics.Color +import android.net.* +import android.net.VpnService +import android.os.* +import android.support.annotation.RequiresApi +import android.support.v4.app.NotificationCompat +import com.v2ray.ang.AppConfig +import com.v2ray.ang.R +import com.v2ray.ang.extension.defaultDPreference +import com.v2ray.ang.extension.toSpeedString +import com.v2ray.ang.ui.MainActivity +import com.v2ray.ang.ui.PerAppProxyActivity +import com.v2ray.ang.ui.SettingsActivity +import com.v2ray.ang.util.MessageUtil +import com.v2ray.ang.util.Utils +import libv2ray.Libv2ray +import libv2ray.V2RayVPNServiceSupportsSet +import rx.Observable +import rx.Subscription +import java.net.InetAddress +import java.io.IOException +import java.io.File +import java.io.FileDescriptor +import java.io.FileInputStream +import java.lang.ref.SoftReference +import android.os.Build +import android.annotation.TargetApi +import android.util.Log +import org.jetbrains.anko.doAsync + +class V2RayVpnService : VpnService() { + companion object { + const val NOTIFICATION_ID = 1 + const val NOTIFICATION_PENDING_INTENT_CONTENT = 0 + const val NOTIFICATION_PENDING_INTENT_STOP_V2RAY = 1 + + fun startV2Ray(context: Context) { + val intent = Intent(context.applicationContext, V2RayVpnService::class.java) + if (Build.VERSION.SDK_INT > Build.VERSION_CODES.N_MR1) { + context.startForegroundService(intent) + } else { + context.startService(intent) + } + } + } + + private val v2rayPoint = Libv2ray.newV2RayPoint(V2RayCallback()) + private lateinit var configContent: String + private lateinit var mInterface: ParcelFileDescriptor + val fd: Int get() = mInterface.fd + private var mBuilder: NotificationCompat.Builder? = null + private var mSubscription: Subscription? = null + private var mNotificationManager: NotificationManager? = null + + + + /** + * 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 + */ + @TargetApi(28) + private val defaultNetworkRequest = NetworkRequest.Builder() + .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) + .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED) + .build() + + + private val connectivity by lazy { getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager } + @TargetApi(28) + private val defaultNetworkCallback = object : ConnectivityManager.NetworkCallback() { + 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) + } + } + private var listeningForDefaultNetwork = false + + override fun onCreate() { + super.onCreate() + + val policy = StrictMode.ThreadPolicy.Builder().permitAll().build() + StrictMode.setThreadPolicy(policy) + v2rayPoint.packageName = Utils.packagePath(applicationContext) + Seq.setContext(applicationContext) + } + + override fun onRevoke() { + stopV2Ray() + } + + override fun onLowMemory() { + stopV2Ray() + super.onLowMemory() + } + + override fun onDestroy() { + super.onDestroy() + cancelNotification() + } + + fun setup(parameters: String) { + + val prepare = VpnService.prepare(this) + if (prepare != null) { + return + } + + // If the old interface has exactly the same parameters, use it! + // Configure a builder while parsing the parameters. + val builder = Builder() + val enableLocalDns = defaultDPreference.getPrefBoolean(SettingsActivity.PREF_LOCAL_DNS_ENABLED, false) + + parameters.split(" ") + .map { it.split(",") } + .forEach { + when (it[0][0]) { + 'm' -> builder.setMtu(java.lang.Short.parseShort(it[1]).toInt()) + 's' -> builder.addSearchDomain(it[1]) + 'a' -> builder.addAddress(it[1], Integer.parseInt(it[2])) + 'r' -> builder.addRoute(it[1], Integer.parseInt(it[2])) + 'd' -> builder.addDnsServer(it[1]) + } + } + + if(!enableLocalDns) { + Utils.getRemoteDnsServers(defaultDPreference) + .forEach { + builder.addDnsServer(it) + } + } + + builder.setSession(defaultDPreference.getPrefString(AppConfig.PREF_CURR_CONFIG_NAME, "")) + + 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) + apps?.forEach { + try { + if (bypassApps) + builder.addDisallowedApplication(it) + else + builder.addAllowedApplication(it) + } catch (e: PackageManager.NameNotFoundException) { + //Logger.d(e) + } + } + } + + // Close the old interface since the parameters have been changed. + try { + mInterface.close() + } catch (ignored: Exception) { + } + + + if (Build.VERSION.SDK_INT >= 28) { + connectivity.requestNetwork(defaultNetworkRequest, defaultNetworkCallback) + listeningForDefaultNetwork = true + } + + // Create a new interface using the builder and save the parameters. + mInterface = builder.establish() + sendFd() + startSpeedNotification() + } + + fun shutdown() { + stopV2Ray(true) + } + + fun sendFd() { + val fd = mInterface.fileDescriptor + val path = File(Utils.packagePath(applicationContext), "sock_path").absolutePath + + doAsync { + var tries = 0 + while (true) try { + Thread.sleep(50L shl tries) + Log.d(packageName, "sendFd tries: " + tries.toString()) + LocalSocket().use { localSocket -> + localSocket.connect(LocalSocketAddress(path, LocalSocketAddress.Namespace.FILESYSTEM)) + localSocket.setFileDescriptorsForSend(arrayOf(fd)) + localSocket.outputStream.write(42) + } + break + } catch (e: Exception) { + Log.d(packageName, e.toString()) + if (tries > 5) break + tries += 1 + } + } + } + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + startV2ray() + return START_STICKY + //return super.onStartCommand(intent, flags, startId) + } + + private fun startV2ray() { + if (!v2rayPoint.isRunning) { + + try { + val mFilter = IntentFilter(AppConfig.BROADCAST_ACTION_SERVICE) + mFilter.addAction(Intent.ACTION_SCREEN_ON) + mFilter.addAction(Intent.ACTION_SCREEN_OFF) + mFilter.addAction(Intent.ACTION_USER_PRESENT) + registerReceiver(mMsgReceive, mFilter) + } catch (e: Exception) { + } + + configContent = defaultDPreference.getPrefString(AppConfig.PREF_CURR_CONFIG, "") + v2rayPoint.configureFileContent = configContent + v2rayPoint.enableLocalDNS = defaultDPreference.getPrefBoolean(SettingsActivity.PREF_LOCAL_DNS_ENABLED, false) + v2rayPoint.forwardIpv6 = defaultDPreference.getPrefBoolean(SettingsActivity.PREF_FORWARD_IPV6, false) + v2rayPoint.domainName = defaultDPreference.getPrefString(AppConfig.PREF_CURR_CONFIG_DOMAIN, "") + + try { + v2rayPoint.runLoop() + } catch (e: Exception) { + Log.d(packageName, e.toString()) + } + + if (v2rayPoint.isRunning) { + MessageUtil.sendMsg2UI(this, AppConfig.MSG_STATE_START_SUCCESS, "") + showNotification() + } else { + MessageUtil.sendMsg2UI(this, AppConfig.MSG_STATE_START_FAILURE, "") + cancelNotification() + } + } + // showNotification() + } + + private fun stopV2Ray(isForced: Boolean = true) { +// val configName = defaultDPreference.getPrefString(PREF_CURR_CONFIG_GUID, "") +// val emptyInfo = VpnNetworkInfo() +// val info = loadVpnNetworkInfo(configName, emptyInfo)!! + (lastNetworkInfo ?: emptyInfo) +// saveVpnNetworkInfo(configName, info) + if (Build.VERSION.SDK_INT >= 28) { + if (listeningForDefaultNetwork) { + connectivity.unregisterNetworkCallback(defaultNetworkCallback) + listeningForDefaultNetwork = false + } + } + if (v2rayPoint.isRunning) { + try { + v2rayPoint.stopLoop() + } catch (e: Exception) { + Log.d(packageName, e.toString()) + } + } + + MessageUtil.sendMsg2UI(this, AppConfig.MSG_STATE_STOP_SUCCESS, "") + cancelNotification() + + if (isForced) { + try { + unregisterReceiver(mMsgReceive) + } catch (e: Exception) { + } + try { + mInterface.close() + } catch (ignored: Exception) { + } + + stopSelf() + } + } + + private fun showNotification() { + val startMainIntent = Intent(applicationContext, MainActivity::class.java) + val contentPendingIntent = PendingIntent.getActivity(applicationContext, + NOTIFICATION_PENDING_INTENT_CONTENT, startMainIntent, + PendingIntent.FLAG_UPDATE_CURRENT) + + val stopV2RayIntent = Intent(AppConfig.BROADCAST_ACTION_SERVICE) + stopV2RayIntent.`package` = AppConfig.ANG_PACKAGE + stopV2RayIntent.putExtra("key", AppConfig.MSG_STATE_STOP) + + val stopV2RayPendingIntent = PendingIntent.getBroadcast(applicationContext, + NOTIFICATION_PENDING_INTENT_STOP_V2RAY, stopV2RayIntent, + PendingIntent.FLAG_UPDATE_CURRENT) + + val channelId = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + createNotificationChannel() + } else { + // If earlier version channel ID is not used + // https://developer.android.com/reference/android/support/v4/app/NotificationCompat.Builder.html#NotificationCompat.Builder(android.content.Context) + "" + } + + mBuilder = NotificationCompat.Builder(applicationContext, channelId) + .setSmallIcon(R.drawable.ic_v) + .setContentTitle(defaultDPreference.getPrefString(AppConfig.PREF_CURR_CONFIG_NAME, "")) + .setPriority(NotificationCompat.PRIORITY_MIN) + .setOngoing(true) + .setShowWhen(false) + .setOnlyAlertOnce(true) + .setContentIntent(contentPendingIntent) + .addAction(R.drawable.ic_close_grey_800_24dp, + getString(R.string.notification_action_stop_v2ray), + stopV2RayPendingIntent) + //.build() + + //mBuilder?.setDefaults(NotificationCompat.FLAG_ONLY_ALERT_ONCE) //取消震动,铃声其他都不好使 + + startForeground(NOTIFICATION_ID, mBuilder?.build()) + } + + @RequiresApi(Build.VERSION_CODES.O) + private fun createNotificationChannel(): String { + val channelId = "RAY_NG_M_CH_ID" + val channelName = "V2rayNG Background Service" + val chan = NotificationChannel(channelId, + channelName, NotificationManager.IMPORTANCE_HIGH) + chan.lightColor = Color.DKGRAY + chan.importance = NotificationManager.IMPORTANCE_NONE + chan.lockscreenVisibility = Notification.VISIBILITY_PRIVATE + getNotificationManager().createNotificationChannel(chan) + return channelId + } + + private fun cancelNotification() { + stopForeground(true) + mBuilder = null + mSubscription?.unsubscribe() + mSubscription = null + } + + private fun updateNotification(contentText: String) { + if (mBuilder != null) { + mBuilder?.setContentTitle(contentText) + getNotificationManager().notify(NOTIFICATION_ID, mBuilder?.build()) + } + } + + private fun getNotificationManager(): NotificationManager { + if (mNotificationManager == null) { + mNotificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + } + return mNotificationManager!! + } + + fun startSpeedNotification() { + if (mSubscription == null && + v2rayPoint.isRunning && + defaultDPreference.getPrefBoolean(SettingsActivity.PREF_SPEED_ENABLED, false)) { + val cf_name = defaultDPreference.getPrefString(AppConfig.PREF_CURR_CONFIG_NAME, "") + var last_zero_speed = false + + mSubscription = Observable.interval(3, java.util.concurrent.TimeUnit.SECONDS) + .subscribe { + val uplink = v2rayPoint.queryStats("socks", "uplink") + val downlink = v2rayPoint.queryStats("socks", "downlink") + val zero_speed = (uplink == 0L && downlink == 0L) + if (!zero_speed || !last_zero_speed) { + updateNotification("${cf_name} • ${(uplink / 3).toSpeedString()}↑ ${(downlink / 3).toSpeedString()}↓") + } + last_zero_speed = zero_speed + } + } + } + + + fun stopSpeedNotification() { + if (mSubscription != null) { + mSubscription?.unsubscribe() //stop queryStats + mSubscription = null + + val cf_name = defaultDPreference.getPrefString(AppConfig.PREF_CURR_CONFIG_NAME, "") + updateNotification(cf_name) + } + } + + private inner class V2RayCallback : V2RayVPNServiceSupportsSet { + override fun shutdown(): Long { + // called by go + // shutdown the whole vpn service + try { + this@V2RayVpnService.shutdown() + return 0 + } catch (e: Exception) { + Log.d(packageName, e.toString()) + return -1 + } + } + + override fun prepare(): Long { + return 0 + } + + override fun protect(l: Long) = (if (this@V2RayVpnService.protect(l.toInt())) 0 else 1).toLong() + + override fun onEmitStatus(l: Long, s: String?): Long { + //Logger.d(s) + return 0 + } + + override fun setup(s: String): Long { + //Logger.d(s) + try { + this@V2RayVpnService.setup(s) + return 0 + } catch (e: Exception) { + Log.d(packageName, e.toString()) + return -1 + } + } + + override fun sendFd(): Long { + try { + this@V2RayVpnService.sendFd() + } catch (e: Exception) { + Log.d(packageName, e.toString()) + return -1 + } + return 0 + } + } + + private var mMsgReceive = ReceiveMessageHandler(this@V2RayVpnService) + + private class ReceiveMessageHandler(vpnService: V2RayVpnService) : BroadcastReceiver() { + internal var mReference: SoftReference = SoftReference(vpnService) + + override fun onReceive(ctx: Context?, intent: Intent?) { + val vpnService = mReference.get() + when (intent?.getIntExtra("key", 0)) { + AppConfig.MSG_REGISTER_CLIENT -> { + //Logger.e("ReceiveMessageHandler", intent?.getIntExtra("key", 0).toString()) + + val isRunning = vpnService?.v2rayPoint!!.isRunning + && VpnService.prepare(vpnService) == null + if (isRunning) { + MessageUtil.sendMsg2UI(vpnService, AppConfig.MSG_STATE_RUNNING, "") + } else { + MessageUtil.sendMsg2UI(vpnService, AppConfig.MSG_STATE_NOT_RUNNING, "") + } + } + AppConfig.MSG_UNREGISTER_CLIENT -> { +// vpnService?.mMsgSend = null + } + AppConfig.MSG_STATE_START -> { + //nothing to do + } + AppConfig.MSG_STATE_STOP -> { + vpnService?.stopV2Ray() + } + AppConfig.MSG_STATE_RESTART -> { + vpnService?.startV2ray() + } + } + + when (intent?.action) { + Intent.ACTION_SCREEN_OFF -> { + Log.d(AppConfig.ANG_PACKAGE, "SCREEN_OFF, stop querying stats") + vpnService?.stopSpeedNotification() + } + Intent.ACTION_SCREEN_ON -> { + Log.d(AppConfig.ANG_PACKAGE, "SCREEN_ON, start querying stats") + vpnService?.startSpeedNotification() + } + } + } + } +} + diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/BaseActivity.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/BaseActivity.kt new file mode 100644 index 000000000..8ea4d87c0 --- /dev/null +++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/BaseActivity.kt @@ -0,0 +1,14 @@ +package com.v2ray.ang.ui + +import android.support.v7.app.AppCompatActivity +import android.view.MenuItem + +abstract class BaseActivity : AppCompatActivity() { + override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) { + android.R.id.home -> { + onBackPressed() + true + } + else -> super.onOptionsItemSelected(item) + } +} \ No newline at end of file diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/BaseDrawerActivity.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/BaseDrawerActivity.kt new file mode 100644 index 000000000..e427c7ed3 --- /dev/null +++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/BaseDrawerActivity.kt @@ -0,0 +1,206 @@ +package com.v2ray.ang.ui + +import android.app.ActivityOptions +import android.app.FragmentManager +import android.content.Intent +import android.content.res.Configuration +import android.os.Bundle +import android.support.design.widget.NavigationView +import android.support.v4.view.GravityCompat +import android.support.v4.widget.DrawerLayout +import android.support.v7.app.ActionBarDrawerToggle +import android.support.v7.widget.Toolbar +import android.util.Log +import android.view.MenuItem +import android.view.View +//import com.v2ray.ang.InappBuyActivity + +import com.v2ray.ang.R +import org.jetbrains.anko.startActivity + + +abstract class BaseDrawerActivity : BaseActivity() { + companion object { + + private val TAG = "BaseDrawerActivity" + } + + private var mToolbar: Toolbar? = null + + private var mDrawerToggle: ActionBarDrawerToggle? = null + + private var mDrawerLayout: DrawerLayout? = null + + private var mToolbarInitialized: Boolean = false + + private var mItemToOpenWhenDrawerCloses = -1 + + private val backStackChangedListener = FragmentManager.OnBackStackChangedListener { updateDrawerToggle() } + + private val drawerListener = object : DrawerLayout.DrawerListener { + override fun onDrawerSlide(drawerView: View, slideOffset: Float) { + mDrawerToggle!!.onDrawerSlide(drawerView, slideOffset) + } + + override fun onDrawerOpened(drawerView: View) { + mDrawerToggle!!.onDrawerOpened(drawerView) + //supportActionBar!!.setTitle(R.string.app_name) + } + + override fun onDrawerClosed(drawerView: View) { + mDrawerToggle!!.onDrawerClosed(drawerView) + + if (mItemToOpenWhenDrawerCloses >= 0) { + val extras = ActivityOptions.makeCustomAnimation( + this@BaseDrawerActivity, R.anim.fade_in, R.anim.fade_out).toBundle() + var activityClass: Class<*>? = null + when (mItemToOpenWhenDrawerCloses) { + R.id.server_profile -> activityClass = MainActivity::class.java + R.id.sub_setting -> activityClass = SubSettingActivity::class.java + R.id.settings -> activityClass = SettingsActivity::class.java + R.id.logcat -> { + startActivity() + return + } + R.id.donate -> { +// startActivity() + 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(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(R.id.drawer_layout) as DrawerLayout + if (mDrawerLayout != null) { + val navigationView = findViewById(R.id.nav_view) as NavigationView + ?: throw IllegalStateException("Layout requires a NavigationView " + "with id 'nav_view'") + + // Create an ActionBarDrawerToggle that will handle opening/closing of the drawer: + mDrawerToggle = ActionBarDrawerToggle(this, mDrawerLayout, + mToolbar, R.string.navigation_drawer_open, R.string.navigation_drawer_close) + + mDrawerLayout!!.addDrawerListener(drawerListener) + + populateDrawerItems(navigationView) + setSupportActionBar(mToolbar) + updateDrawerToggle() + } else { + setSupportActionBar(mToolbar) + } + + mToolbarInitialized = true + } + + private fun populateDrawerItems(navigationView: NavigationView) { + navigationView.setNavigationItemSelectedListener { menuItem -> + menuItem.isChecked = true + mItemToOpenWhenDrawerCloses = menuItem.itemId + mDrawerLayout!!.closeDrawers() + true + } + + if (MainActivity::class.java.isAssignableFrom(javaClass)) { + navigationView.setCheckedItem(R.id.server_profile) + } else if (SubSettingActivity::class.java.isAssignableFrom(javaClass)) { + navigationView.setCheckedItem(R.id.sub_setting) + } else if (SettingsActivity::class.java.isAssignableFrom(javaClass)) { + navigationView.setCheckedItem(R.id.settings) + } + } +} diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/FragmentAdapter.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/FragmentAdapter.kt new file mode 100644 index 000000000..e4d34e3f0 --- /dev/null +++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/FragmentAdapter.kt @@ -0,0 +1,21 @@ +package com.v2ray.ang.ui + + +import android.support.v4.app.Fragment +import android.support.v4.app.FragmentManager +import android.support.v4.app.FragmentStatePagerAdapter + +class FragmentAdapter(fm: FragmentManager, private val mFragments: List, private val mTitles: List) : FragmentStatePagerAdapter(fm) { + + override fun getItem(position: Int): Fragment { + return mFragments[position] + } + + override fun getCount(): Int { + return mFragments.size + } + + override fun getPageTitle(position: Int): CharSequence? { + return mTitles[position] + } +} \ No newline at end of file diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/LogcatActivity.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/LogcatActivity.kt new file mode 100644 index 000000000..872f32ea5 --- /dev/null +++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/LogcatActivity.kt @@ -0,0 +1,72 @@ +package com.v2ray.ang.ui + +import android.os.Bundle +import android.text.method.ScrollingMovementMethod +import android.view.Menu +import android.view.MenuItem +import android.view.View +import com.v2ray.ang.R +import com.v2ray.ang.util.Utils +import kotlinx.android.synthetic.main.activity_logcat.* +import org.jetbrains.anko.doAsync +import org.jetbrains.anko.toast +import org.jetbrains.anko.uiThread + +import java.io.IOException +import java.util.LinkedHashSet + +class LogcatActivity : BaseActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_logcat) + + title = getString(R.string.title_logcat) + + supportActionBar?.setDisplayHomeAsUpEnabled(true) + logcat() + } + + private fun logcat() { + + try { + pb_waiting.visibility = View.VISIBLE + + doAsync { + val lst = LinkedHashSet() + lst.add("logcat") + lst.add("-d") + lst.add("-v") + lst.add("time") + lst.add("-s") + lst.add("GoLog,tun2socks,com.v2ray.ang") + val process = Runtime.getRuntime().exec(lst.toTypedArray()) +// val bufferedReader = BufferedReader( +// InputStreamReader(process.inputStream)) +// val allText = bufferedReader.use(BufferedReader::readText) + val allText = process.inputStream.bufferedReader().use { it.readText() } + uiThread { + tv_logcat.text = allText + tv_logcat.movementMethod = ScrollingMovementMethod() + pb_waiting.visibility = View.GONE + } + } + } catch (e: IOException) { + } + } + + 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()) + toast(R.string.toast_success) + true + } + + else -> super.onOptionsItemSelected(item) + } +} diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/MainActivity.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/MainActivity.kt new file mode 100644 index 000000000..7c717baa1 --- /dev/null +++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/MainActivity.kt @@ -0,0 +1,573 @@ +package com.v2ray.ang.ui + +import android.Manifest +import android.content.* +import android.net.Uri +import android.net.VpnService +import android.support.v7.widget.LinearLayoutManager +import android.view.Menu +import android.view.MenuItem +import com.tbruyelle.rxpermissions.RxPermissions +import com.v2ray.ang.R +import com.v2ray.ang.util.AngConfigManager +import com.v2ray.ang.util.Utils +import kotlinx.android.synthetic.main.activity_main.* +import android.os.Bundle +import android.text.TextUtils +import android.view.KeyEvent +import com.v2ray.ang.AppConfig +import com.v2ray.ang.util.MessageUtil +import com.v2ray.ang.util.V2rayConfigUtil +import org.jetbrains.anko.* +import java.lang.ref.SoftReference +import java.net.URL +import android.content.IntentFilter +import android.support.design.widget.NavigationView +import android.support.v4.view.GravityCompat +import android.support.v7.app.ActionBarDrawerToggle +import android.support.v7.widget.helper.ItemTouchHelper +import android.util.Log +//import com.v2ray.ang.InappBuyActivity +import rx.Observable +import rx.android.schedulers.AndroidSchedulers +import java.util.concurrent.TimeUnit +import com.v2ray.ang.helper.SimpleItemTouchHelperCallback +import com.v2ray.ang.util.AngConfigManager.configs + +class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedListener { + companion object { + private const val REQUEST_CODE_VPN_PREPARE = 0 + private const val REQUEST_SCAN = 1 + private const val REQUEST_FILE_CHOOSER = 2 + private const val REQUEST_SCAN_URL = 3 + } + + var isRunning = false + set(value) { + field = value + adapter.changeable = !value + if (value) { + fab.imageResource = R.drawable.ic_v + tv_test_state.text = getString(R.string.connection_connected) + } else { + fab.imageResource = R.drawable.ic_v_idle + tv_test_state.text = getString(R.string.connection_not_connected) + } + hideCircle() + } + + private val adapter by lazy { MainRecyclerAdapter(this) } + private var mItemTouchHelper: ItemTouchHelper? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_main) + title = getString(R.string.title_server) + setSupportActionBar(toolbar) + + fab.setOnClickListener { + if (isRunning) { + Utils.stopVService(this) + } else { + val intent = VpnService.prepare(this) + if (intent == null) { + startV2Ray() + } else { + startActivityForResult(intent, REQUEST_CODE_VPN_PREPARE) + } + } + } + layout_test.setOnClickListener { + if (isRunning) { + val socksPort = 10808//Utils.parseInt(defaultDPreference.getPrefString(SettingsActivity.PREF_SOCKS_PORT, "10808")) + + tv_test_state.text = getString(R.string.connection_test_testing) + doAsync { + val result = Utils.testConnection(this@MainActivity, socksPort) + uiThread { + tv_test_state.text = Utils.getEditable(result) + } + } + } else { +// tv_test_state.text = getString(R.string.connection_test_fail) + } + } + + recycler_view.setHasFixedSize(true) + recycler_view.layoutManager = LinearLayoutManager(this) + recycler_view.adapter = adapter + + val callback = SimpleItemTouchHelperCallback(adapter) + mItemTouchHelper = ItemTouchHelper(callback) + mItemTouchHelper?.attachToRecyclerView(recycler_view) + + + val toggle = ActionBarDrawerToggle( + this, drawer_layout, toolbar, R.string.navigation_drawer_open, R.string.navigation_drawer_close) + drawer_layout.addDrawerListener(toggle) + toggle.syncState() + nav_view.setNavigationItemSelectedListener(this) + } + + fun startV2Ray() { + if (AngConfigManager.configs.index < 0) { + return + } + showCircle() +// toast(R.string.toast_services_start) + if (!Utils.startVService(this)) { + hideCircle() + } + } + + override fun onStart() { + super.onStart() + isRunning = false + +// val intent = Intent(this.applicationContext, V2RayVpnService::class.java) +// intent.`package` = AppConfig.ANG_PACKAGE +// bindService(intent, mConnection, BIND_AUTO_CREATE) + + mMsgReceive = ReceiveMessageHandler(this@MainActivity) + registerReceiver(mMsgReceive, IntentFilter(AppConfig.BROADCAST_ACTION_ACTIVITY)) + MessageUtil.sendMsg2Service(this, AppConfig.MSG_REGISTER_CLIENT, "") + } + + override fun onStop() { + super.onStop() + if (mMsgReceive != null) { + unregisterReceiver(mMsgReceive) + mMsgReceive = null + } + } + + public override fun onResume() { + super.onResume() + adapter.updateConfigList() + } + + public override fun onPause() { + super.onPause() + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + when (requestCode) { + REQUEST_CODE_VPN_PREPARE -> + if (resultCode == RESULT_OK) { + startV2Ray() + } + REQUEST_SCAN -> + if (resultCode == RESULT_OK) { + importBatchConfig(data?.getStringExtra("SCAN_RESULT")) + } + REQUEST_FILE_CHOOSER -> { + if (resultCode == RESULT_OK) { + val uri = data!!.data + readContentFromUri(uri) + } + } + REQUEST_SCAN_URL -> + if (resultCode == RESULT_OK) { + importConfigCustomUrl(data?.getStringExtra("SCAN_RESULT")) + } + } + } + + override fun onCreateOptionsMenu(menu: Menu?): Boolean { + menuInflater.inflate(R.menu.menu_main, menu) + return true + } + + override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) { + R.id.import_qrcode -> { + importQRcode(REQUEST_SCAN) + true + } + R.id.import_clipboard -> { + importClipboard() + true + } + R.id.import_manually_vmess -> { + startActivity("position" to -1, "isRunning" to isRunning) + adapter.updateConfigList() + true + } + R.id.import_manually_ss -> { + startActivity("position" to -1, "isRunning" to isRunning) + adapter.updateConfigList() + true + } + R.id.import_manually_socks -> { + startActivity("position" to -1, "isRunning" to isRunning) + adapter.updateConfigList() + true + } + R.id.import_config_custom_clipboard -> { + importConfigCustomClipboard() + true + } + R.id.import_config_custom_local -> { + importConfigCustomLocal() + true + } + R.id.import_config_custom_url -> { + importConfigCustomUrlClipboard() + true + } + R.id.import_config_custom_url_scan -> { + importQRcode(REQUEST_SCAN_URL) + true + } + +// R.id.sub_setting -> { +// startActivity() +// true +// } + + R.id.sub_update -> { + importConfigViaSub() + true + } + + R.id.export_all -> { + if (AngConfigManager.shareAll2Clipboard() == 0) { + toast(R.string.toast_success) + } else { + toast(R.string.toast_failure) + } + true + } + + R.id.ping_all -> { + for (k in 0 until configs.vmess.count()) { + configs.vmess[k].testResult = "" + adapter.updateConfigList() + } + for (k in 0 until configs.vmess.count()) { + if (configs.vmess[k].configType != AppConfig.EConfigType.Custom) { + doAsync { + configs.vmess[k].testResult = Utils.tcping(configs.vmess[k].address, configs.vmess[k].port) + uiThread { + adapter.updateSelectedItem(k) + } + } + } + } + true + } + +// R.id.settings -> { +// startActivity("isRunning" to isRunning) +// true +// } +// R.id.logcat -> { +// startActivity() +// true +// } + else -> super.onOptionsItemSelected(item) + } + + + /** + * import config from qrcode + */ + fun importQRcode(requestCode: Int): Boolean { +// try { +// startActivityForResult(Intent("com.google.zxing.client.android.SCAN") +// .addCategory(Intent.CATEGORY_DEFAULT) +// .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP), requestCode) +// } catch (e: Exception) { + RxPermissions(this) + .request(Manifest.permission.CAMERA) + .subscribe { + if (it) + startActivityForResult(requestCode) + else + toast(R.string.toast_permission_denied) + } +// } + return true + } + + /** + * import config from clipboard + */ + fun importClipboard() + : Boolean { + try { + val clipboard = Utils.getClipboard(this) + importBatchConfig(clipboard) + } catch (e: Exception) { + e.printStackTrace() + return false + } + return true + } + + fun importBatchConfig(server: String?, subid: String = "") { + val count = AngConfigManager.importBatchConfig(server, subid) + if (count > 0) { + toast(R.string.toast_success) + adapter.updateConfigList() + } else { + toast(R.string.toast_failure) + } + } + + fun importConfigCustomClipboard() + : Boolean { + try { + val configText = Utils.getClipboard(this) + if (TextUtils.isEmpty(configText)) { + toast(R.string.toast_none_data_clipboard) + return false + } + importCustomizeConfig(configText) + return true + } catch (e: Exception) { + e.printStackTrace() + return false + } + } + + /** + * import config from local config file + */ + fun importConfigCustomLocal(): Boolean { + try { + showFileChooser() + } catch (e: Exception) { + e.printStackTrace() + return false + } + return true + } + + fun importConfigCustomUrlClipboard() + : Boolean { + try { + val url = Utils.getClipboard(this) + if (TextUtils.isEmpty(url)) { + toast(R.string.toast_none_data_clipboard) + return false + } + return importConfigCustomUrl(url) + } catch (e: Exception) { + e.printStackTrace() + return false + } + } + + /** + * import config from url + */ + fun importConfigCustomUrl(url: String?): Boolean { + try { + if (!Utils.isValidUrl(url)) { + toast(R.string.toast_invalid_url) + return false + } + doAsync { + val configText = URL(url).readText() + uiThread { + importCustomizeConfig(configText) + } + } + } catch (e: Exception) { + e.printStackTrace() + return false + } + return true + } + + /** + * import config from sub + */ + fun importConfigViaSub() + : Boolean { + try { + toast(R.string.title_sub_update) + 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) + ) { + continue + } + val id = subItem[k].id + val url = subItem[k].url + if (!Utils.isValidUrl(url)) { + continue + } + Log.d("Main", url) + doAsync { + val configText = URL(url).readText() + uiThread { + importBatchConfig(Utils.decode(configText), id) + } + } + } + } catch (e: Exception) { + e.printStackTrace() + return false + } + return true + } + + /** + * show file chooser + */ + private fun showFileChooser() { + val intent = Intent(Intent.ACTION_GET_CONTENT) + intent.type = "*/*" + intent.addCategory(Intent.CATEGORY_OPENABLE) + + try { + startActivityForResult( + Intent.createChooser(intent, getString(R.string.title_file_chooser)), + REQUEST_FILE_CHOOSER) + } catch (ex: android.content.ActivityNotFoundException) { + toast(R.string.toast_require_file_manager) + } + } + + /** + * read content from uri + */ + private fun readContentFromUri(uri: Uri) { + RxPermissions(this) + .request(Manifest.permission.READ_EXTERNAL_STORAGE) + .subscribe { + if (it) { + try { + val inputStream = contentResolver.openInputStream(uri) + val configText = inputStream.bufferedReader().readText() + importCustomizeConfig(configText) + } catch (e: Exception) { + e.printStackTrace() + } + } else + toast(R.string.toast_permission_denied) + } + } + + /** + * 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 { + toast(R.string.toast_success) + adapter.updateConfigList() + } + } + +// val mConnection = object : ServiceConnection { +// override fun onServiceDisconnected(name: ComponentName?) { +// } +// +// override fun onServiceConnected(name: ComponentName?, service: IBinder?) { +// sendMsg(AppConfig.MSG_REGISTER_CLIENT, "") +// } +// } + + private + var mMsgReceive: BroadcastReceiver? = null + + private class ReceiveMessageHandler(activity: MainActivity) : BroadcastReceiver() { + internal var mReference: SoftReference = SoftReference(activity) + override fun onReceive(ctx: Context?, intent: Intent?) { + val activity = mReference.get() + when (intent?.getIntExtra("key", 0)) { + AppConfig.MSG_STATE_RUNNING -> { + activity?.isRunning = true + } + AppConfig.MSG_STATE_NOT_RUNNING -> { + activity?.isRunning = false + } + AppConfig.MSG_STATE_START_SUCCESS -> { + activity?.toast(R.string.toast_services_success) + activity?.isRunning = true + } + AppConfig.MSG_STATE_START_FAILURE -> { + activity?.toast(R.string.toast_services_failure) + activity?.isRunning = false + } + AppConfig.MSG_STATE_STOP_SUCCESS -> { + activity?.isRunning = false + } + } + } + } + + override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean { + if (keyCode == KeyEvent.KEYCODE_BACK) { + moveTaskToBack(false) + return true + } + return super.onKeyDown(keyCode, event) + } + + fun showCircle() { + fabProgressCircle?.show() + } + + fun hideCircle() { + try { + Observable.timer(300, TimeUnit.MILLISECONDS) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe { + if (fabProgressCircle.isShown) { + fabProgressCircle.hide() + } + } + } catch (e: Exception) { + } + } + + override fun onBackPressed() { + if (drawer_layout.isDrawerOpen(GravityCompat.START)) { + drawer_layout.closeDrawer(GravityCompat.START) + } else { + super.onBackPressed() + } + } + + override fun onNavigationItemSelected(item: MenuItem): Boolean { + // Handle navigation view item clicks here. + when (item.itemId) { + //R.id.server_profile -> activityClass = MainActivity::class.java + R.id.sub_setting -> { + startActivity() + } + R.id.settings -> { + startActivity("isRunning" to isRunning) + } + R.id.feedback -> { + Utils.openUri(this, AppConfig.v2rayNGIssues) + } + R.id.promotion -> { + Utils.openUri(this, AppConfig.promotionUrl) + } + R.id.donate -> { +// startActivity() + } + R.id.logcat -> { + startActivity() + } + } + drawer_layout.closeDrawer(GravityCompat.START) + return true + } +} \ No newline at end of file diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/MainRecyclerAdapter.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/MainRecyclerAdapter.kt new file mode 100644 index 000000000..4ae5aadd4 --- /dev/null +++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/MainRecyclerAdapter.kt @@ -0,0 +1,268 @@ +package com.v2ray.ang.ui + +import android.graphics.Color +import android.support.v7.widget.RecyclerView +import android.text.TextUtils +import android.view.View +import android.view.ViewGroup +import com.v2ray.ang.AppConfig +import com.v2ray.ang.R +import com.v2ray.ang.dto.AngConfig +import com.v2ray.ang.helper.ItemTouchHelperAdapter +import com.v2ray.ang.helper.ItemTouchHelperViewHolder +import com.v2ray.ang.util.AngConfigManager +import com.v2ray.ang.util.Utils +import kotlinx.android.synthetic.main.item_qrcode.view.* +import kotlinx.android.synthetic.main.item_recycler_main.view.* +import org.jetbrains.anko.* +import rx.Observable +import rx.android.schedulers.AndroidSchedulers +import java.util.concurrent.TimeUnit +import com.v2ray.ang.extension.defaultDPreference + +class MainRecyclerAdapter(val activity: MainActivity) : RecyclerView.Adapter() + , ItemTouchHelperAdapter { + companion object { + private const val VIEW_TYPE_ITEM = 1 + private const val VIEW_TYPE_FOOTER = 2 + } + + private var mActivity: MainActivity = activity + private lateinit var configs: AngConfig + private val share_method: Array by lazy { + mActivity.resources.getStringArray(R.array.share_method) + } + + var changeable: Boolean = true + set(value) { + if (field == value) + return + field = value + notifyDataSetChanged() + } + + init { + updateConfigList() + } + + override fun getItemCount() = configs.vmess.count() + 1 + + override fun onBindViewHolder(holder: BaseViewHolder, position: Int) { + if (holder is MainViewHolder) { + val configType = configs.vmess[position].configType + val remarks = configs.vmess[position].remarks + val subid = configs.vmess[position].subid + val address = configs.vmess[position].address + val port = configs.vmess[position].port + val test_result = configs.vmess[position].testResult + + holder.name.text = remarks + holder.radio.isChecked = (position == configs.index) + holder.itemView.backgroundColor = Color.TRANSPARENT + holder.test_result.text = test_result + + if (TextUtils.isEmpty(subid)) { + holder.subid.text = "" + } else { + holder.subid.text = "S" + } + + if (configType == AppConfig.EConfigType.Vmess) { + holder.type.text = "vmess" + holder.statistics.text = "$address : $port" + holder.layout_share.visibility = View.VISIBLE + } else if (configType == AppConfig.EConfigType.Custom) { + holder.type.text = mActivity.getString(R.string.server_customize_config) + holder.statistics.text = ""//mActivity.getString(R.string.server_customize_config) + holder.layout_share.visibility = View.INVISIBLE + } else if (configType == AppConfig.EConfigType.Shadowsocks) { + holder.type.text = "shadowsocks" + holder.statistics.text = "$address : $port" + holder.layout_share.visibility = View.VISIBLE + } else if (configType == AppConfig.EConfigType.Socks) { + holder.type.text = "socks" + holder.statistics.text = "$address : $port" + holder.layout_share.visibility = View.VISIBLE + } + + holder.layout_share.setOnClickListener { + mActivity.selector(null, share_method.asList()) { dialogInterface, i -> + try { + when (i) { + 0 -> { + val iv = mActivity.layoutInflater.inflate(R.layout.item_qrcode, null) + iv.iv_qcode.setImageBitmap(AngConfigManager.share2QRCode(position)) + + mActivity.alert { + customView { + linearLayout { + addView(iv) + } + } + }.show() + } + 1 -> { + if (AngConfigManager.share2Clipboard(position) == 0) { + mActivity.toast(R.string.toast_success) + } else { + mActivity.toast(R.string.toast_failure) + } + } + 2 -> { + if (AngConfigManager.shareFullContent2Clipboard(position) == 0) { + mActivity.toast(R.string.toast_success) + } else { + mActivity.toast(R.string.toast_failure) + } + } + else -> + mActivity.toast("else") + } + } catch (e: Exception) { + e.printStackTrace() + } + } + } + + holder.layout_edit.setOnClickListener { + if (configType == AppConfig.EConfigType.Vmess) { + mActivity.startActivity("position" to position, "isRunning" to !changeable) + } else if (configType == AppConfig.EConfigType.Custom) { + mActivity.startActivity("position" to position, "isRunning" to !changeable) + } else if (configType == AppConfig.EConfigType.Shadowsocks) { + mActivity.startActivity("position" to position, "isRunning" to !changeable) + } else if (configType == AppConfig.EConfigType.Socks) { + mActivity.startActivity("position" to position, "isRunning" to !changeable) + } + } + holder.layout_remove.setOnClickListener { + if (configs.index != position) { + if (AngConfigManager.removeServer(position) == 0) { + notifyItemRemoved(position) + updateSelectedItem(position) + } + } + } + + holder.infoContainer.setOnClickListener { + if (changeable) { + AngConfigManager.setActiveServer(position) + } else { + mActivity.showCircle() + Utils.stopVService(mActivity) + AngConfigManager.setActiveServer(position) + Observable.timer(500, TimeUnit.MILLISECONDS) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe { + mActivity.showCircle() + if (!Utils.startVService(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 + } else { + holder.layout_edit.setOnClickListener { + Utils.openUri(mActivity, AppConfig.promotionUrl) + } + } + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseViewHolder { + when (viewType) { + VIEW_TYPE_ITEM -> + return MainViewHolder(parent.context.layoutInflater + .inflate(R.layout.item_recycler_main, parent, false)) + else -> + return FooterViewHolder(parent.context.layoutInflater + .inflate(R.layout.item_recycler_footer, 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 + } else { + return 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() { + itemView.setBackgroundColor(Color.LTGRAY) + } + + override fun onItemClear() { + itemView.setBackgroundColor(0) + } + } + + class FooterViewHolder(itemView: View) : BaseViewHolder(itemView), ItemTouchHelperViewHolder { + val layout_edit = itemView.layout_edit!! + + override fun onItemSelected() { + itemView.setBackgroundColor(Color.LTGRAY) + } + + override fun onItemClear() { + itemView.setBackgroundColor(0) + } + } + + override fun onItemDismiss(position: Int) { + if (configs.index != position) { +// mActivity.alert(R.string.del_config_comfirm) { +// positiveButton(android.R.string.ok) { + if (AngConfigManager.removeServer(position) == 0) { + notifyItemRemoved(position) + } +// } +// show() +// } + } + updateSelectedItem(position) + } + + override fun onItemMove(fromPosition: Int, toPosition: Int): Boolean { + AngConfigManager.swapServer(fromPosition, toPosition) + notifyItemMoved(fromPosition, toPosition) + //notifyDataSetChanged() + updateSelectedItem(if (fromPosition < toPosition) fromPosition else toPosition) + return true + } +} diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/PerAppProxyActivity.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/PerAppProxyActivity.kt new file mode 100644 index 000000000..3ba3120cc --- /dev/null +++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/PerAppProxyActivity.kt @@ -0,0 +1,279 @@ +package com.v2ray.ang.ui + +import android.animation.Animator +import android.animation.AnimatorListenerAdapter +import android.content.Context +import android.os.Bundle +import android.support.v7.widget.RecyclerView +import android.text.TextUtils +import android.util.Log +import android.view.Menu +import android.view.MenuItem +import android.view.View +import android.view.animation.AccelerateInterpolator +import android.view.animation.DecelerateInterpolator +import com.dinuscxj.itemdecoration.LinearDividerItemDecoration +import 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 com.v2ray.ang.AppConfig +import com.v2ray.ang.dto.AppInfo +import com.v2ray.ang.extension.v2RayApplication +import com.v2ray.ang.util.Utils +import org.jetbrains.anko.doAsync +import org.jetbrains.anko.toast +import org.jetbrains.anko.uiThread +import java.net.URL + +class PerAppProxyActivity : BaseActivity() { + companion object { + const val PREF_PER_APP_PROXY_SET = "pref_per_app_proxy_set" + const val PREF_BYPASS_APPS = "pref_bypass_apps" + } + + private var adapter: PerAppProxyAdapter? = null + private var appsAll: List? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_bypass_list) + + supportActionBar?.setDisplayHomeAsUpEnabled(true) + + val dividerItemDecoration = LinearDividerItemDecoration( + this, LinearDividerItemDecoration.LINEAR_DIVIDER_VERTICAL) + recycler_view.addItemDecoration(dividerItemDecoration) + + val blacklist = defaultDPreference.getPrefStringSet(PREF_PER_APP_PROXY_SET, null) + + AppManagerUtil.rxLoadNetworkAppList(this) + .subscribeOn(Schedulers.io()) + .map { + if (blacklist != null) { + it.forEach { one -> + if ((blacklist.contains(one.packageName))) { + one.isSelected = 1 + } else { + one.isSelected = 0 + } + } + val comparator = object : Comparator { + override fun compare(p1: AppInfo, p2: AppInfo): Int = when { + p1.isSelected > p2.isSelected -> -1 + p1.isSelected == p2.isSelected -> 0 + else -> 1 + } + } + it.sortedWith(comparator) + } else { + val comparator = object : Comparator { + val collator = Collator.getInstance() + override fun compare(o1: AppInfo, o2: AppInfo) = collator.compare(o1.appName, o2.appName) + } + it.sortedWith(comparator) + } + } +// .map { +// val comparator = object : Comparator { +// val collator = Collator.getInstance() +// override fun compare(o1: AppInfo, o2: AppInfo) = collator.compare(o1.appName, o2.appName) +// } +// it.sortedWith(comparator) +// } + .observeOn(AndroidSchedulers.mainThread()) + .subscribe { + appsAll = it + adapter = PerAppProxyAdapter(this, it, blacklist) + recycler_view.adapter = adapter + pb_waiting.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 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 + } + }) + + switch_per_app_proxy.setOnCheckedChangeListener { buttonView, isChecked -> + defaultDPreference.setPrefBoolean(SettingsActivity.PREF_PER_APP_PROXY, isChecked) + } + switch_per_app_proxy.isChecked = defaultDPreference.getPrefBoolean(SettingsActivity.PREF_PER_APP_PROXY, false) + + switch_bypass_apps.setOnCheckedChangeListener { buttonView, isChecked -> + defaultDPreference.setPrefBoolean(PREF_BYPASS_APPS, isChecked) + } + switch_bypass_apps.isChecked = defaultDPreference.getPrefBoolean(PREF_BYPASS_APPS, false) + + et_search.setOnEditorActionListener { v, actionId, event -> + if (actionId == EditorInfo.IME_ACTION_SEARCH) { + //hide + var imm: InputMethodManager = v.context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager + imm.toggleSoftInput(0, InputMethodManager.HIDE_NOT_ALWAYS) + + val key = v.text.toString().toUpperCase() + val apps = ArrayList() + if (TextUtils.isEmpty(key)) { + appsAll?.forEach { + apps.add(it) + } + } else { + appsAll?.forEach { + if (it.appName.toUpperCase().indexOf(key) >= 0) { + apps.add(it) + } + } + } + adapter = PerAppProxyAdapter(this, apps, adapter?.blacklist) + recycler_view.adapter = adapter + adapter?.notifyDataSetChanged() + true + } else { + false + } + } + } + + override fun onPause() { + super.onPause() + adapter?.let { + defaultDPreference.setPrefStringSet(PREF_PER_APP_PROXY_SET, it.blacklist) + } + } + + override fun onCreateOptionsMenu(menu: Menu?): Boolean { + menuInflater.inflate(R.menu.menu_bypass_list, menu) + return super.onCreateOptionsMenu(menu) + } + + override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) { + R.id.select_all -> adapter?.let { + val pkgNames = it.apps.map { it.packageName } + if (it.blacklist.containsAll(pkgNames)) { + it.apps.forEach { + val packageName = it.packageName + adapter?.blacklist!!.remove(packageName) + } + } else { + it.apps.forEach { + val packageName = it.packageName + adapter?.blacklist!!.add(packageName) + } + + } + it.notifyDataSetChanged() + true + } ?: false + R.id.select_proxy_app -> { + selectProxyApp() + + true + } + else -> super.onOptionsItemSelected(item) + } + + private fun selectProxyApp() { + toast(R.string.msg_downloading_content) + val url = AppConfig.androidpackagenamelistUrl + doAsync { + val content = URL(url).readText() + uiThread { + Log.d("selectProxyApp", content) + selectProxyApp(content) + toast(R.string.toast_success) + } + } + } + + private fun selectProxyApp(content: String): Boolean { + try { + var proxyApps = content + if (TextUtils.isEmpty(content)) { + val assets = Utils.readTextFromAssets(v2RayApplication, "proxy_packagename.txt") + proxyApps = assets.lines().toString() + } + if (TextUtils.isEmpty(proxyApps)) { + return false + } + + adapter?.blacklist!!.clear() + + if (switch_bypass_apps.isChecked) { + adapter?.let { + it.apps.forEach block@{ + val packageName = it.packageName + Log.d("selectProxyApp2", packageName) + if (proxyApps.indexOf(packageName) < 0) { + adapter?.blacklist!!.add(packageName) + println(packageName) + return@block + } + } + it.notifyDataSetChanged() + } + } else { + adapter?.let { + it.apps.forEach block@{ + val packageName = it.packageName + Log.d("selectProxyApp3", packageName) + if (proxyApps.indexOf(packageName) >= 0) { + adapter?.blacklist!!.add(packageName) + println(packageName) + return@block + } + } + it.notifyDataSetChanged() + } + } + } catch (e: Exception) { + e.printStackTrace() + return false + } + return true + } +} \ No newline at end of file diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/PerAppProxyAdapter.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/PerAppProxyAdapter.kt new file mode 100644 index 000000000..f2e5eeef9 --- /dev/null +++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/PerAppProxyAdapter.kt @@ -0,0 +1,97 @@ +package com.v2ray.ang.ui + +import android.graphics.Color +import android.support.v7.widget.RecyclerView +import android.view.View +import android.view.ViewGroup +import com.v2ray.ang.R +import com.v2ray.ang.dto.AppInfo +import kotlinx.android.synthetic.main.item_recycler_bypass_list.view.* +import org.jetbrains.anko.image +import org.jetbrains.anko.layoutInflater +import org.jetbrains.anko.textColor +import java.util.* + +class PerAppProxyAdapter(val activity: BaseActivity, val apps: List, blacklist: MutableSet?) : + RecyclerView.Adapter() { + + companion object { + private const val VIEW_TYPE_HEADER = 0 + private const val VIEW_TYPE_ITEM = 1 + } + + private var mActivity: BaseActivity = activity + val blacklist = if (blacklist == null) HashSet() else HashSet(blacklist) + + override fun onBindViewHolder(holder: BaseViewHolder, position: Int) { + if (holder is AppViewHolder) { + val appInfo = apps[position - 1] + holder.bind(appInfo) + } + } + + override fun getItemCount() = apps.size + 1 + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseViewHolder { + val ctx = parent.context + + return when (viewType) { + VIEW_TYPE_HEADER -> { + val view = View(ctx) + view.layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, + ctx.resources.getDimensionPixelSize(R.dimen.bypass_list_header_height) * 3) + BaseViewHolder(view) + } +// VIEW_TYPE_ITEM -> AppViewHolder(ctx.layoutInflater +// .inflate(R.layout.item_recycler_bypass_list, parent, false)) + + else -> AppViewHolder(ctx.layoutInflater + .inflate(R.layout.item_recycler_bypass_list, parent, false)) + + } + } + + override fun getItemViewType(position: Int) = if (position == 0) VIEW_TYPE_HEADER else VIEW_TYPE_ITEM + + open class BaseViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) + + inner class AppViewHolder(itemView: View) : BaseViewHolder(itemView), + View.OnClickListener { + private val inBlacklist: Boolean get() = blacklist.contains(appInfo.packageName) + private lateinit var appInfo: AppInfo + + val icon = itemView.icon!! + val name = itemView.name!! + val package_name = itemView.package_name!! + val checkBox = itemView.check_box!! + + fun bind(appInfo: AppInfo) { + this.appInfo = appInfo + + icon.image = appInfo.appIcon +// name.text = appInfo.appName + + checkBox.isChecked = inBlacklist + package_name.text = appInfo.packageName + if (appInfo.isSystemApp) { + name.text = String.format("** %1s", appInfo.appName) + name.textColor = Color.RED + } else { + name.text = appInfo.appName + name.textColor = Color.DKGRAY + } + + itemView.setOnClickListener(this) + } + + override fun onClick(v: View?) { + if (inBlacklist) { + blacklist.remove(appInfo.packageName) + checkBox.isChecked = false + } else { + blacklist.add(appInfo.packageName) + checkBox.isChecked = true + } + } + } +} \ No newline at end of file diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/RoutingSettingsActivity.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/RoutingSettingsActivity.kt new file mode 100644 index 000000000..43f5ae7ae --- /dev/null +++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/RoutingSettingsActivity.kt @@ -0,0 +1,33 @@ +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 com.v2ray.ang.AppConfig +import kotlinx.android.synthetic.main.activity_routing_settings.* + + +class RoutingSettingsActivity : BaseActivity() { + private val titles: Array by lazy { + resources.getStringArray(R.array.routing_tag) + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_routing_settings) + + title = getString(R.string.routing_settings_title) + supportActionBar?.setDisplayHomeAsUpEnabled(true) + + val fragments = ArrayList() + fragments.add(RoutingSettingsFragment().newInstance(AppConfig.PREF_V2RAY_ROUTING_AGENT)) + fragments.add(RoutingSettingsFragment().newInstance(AppConfig.PREF_V2RAY_ROUTING_DIRECT)) + fragments.add(RoutingSettingsFragment().newInstance(AppConfig.PREF_V2RAY_ROUTING_BLOCKED)) + + val adapter = FragmentAdapter(supportFragmentManager, fragments, titles.toList()) + viewpager?.adapter = adapter + tablayout.setTabTextColors(Color.BLACK, Color.RED) + tablayout.setupWithViewPager(viewpager) + } +} diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/RoutingSettingsFragment.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/RoutingSettingsFragment.kt new file mode 100644 index 000000000..492829ed0 --- /dev/null +++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/RoutingSettingsFragment.kt @@ -0,0 +1,149 @@ +package com.v2ray.ang.ui + + +import android.Manifest +import android.app.Activity.RESULT_OK +import android.content.Intent +import android.os.Bundle +import android.support.v4.app.Fragment +import android.text.TextUtils +import android.util.Log +import android.view.* +import com.v2ray.ang.R +import com.v2ray.ang.extension.defaultDPreference +import com.v2ray.ang.util.Utils +import kotlinx.android.synthetic.main.fragment_routing_settings.* +import org.jetbrains.anko.toast +import android.view.MenuInflater +import com.tbruyelle.rxpermissions.RxPermissions +import com.v2ray.ang.AppConfig +import org.jetbrains.anko.doAsync +import org.jetbrains.anko.startActivityForResult +import org.jetbrains.anko.support.v4.startActivityForResult +import org.jetbrains.anko.support.v4.toast +import org.jetbrains.anko.uiThread +import java.net.URL + + +class RoutingSettingsFragment : Fragment() { + companion object { + private const val routing_arg = "routing_arg" + private const val REQUEST_SCAN_REPLACE = 11 + private const val REQUEST_SCAN_APPEND = 12 + } + + 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) + } + + fun newInstance(arg: String): Fragment { + val fragment = RoutingSettingsFragment() + val bundle = Bundle() + bundle.putString(routing_arg, arg) + fragment.arguments = bundle + return fragment + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + + val content = activity?.defaultDPreference?.getPrefString(arguments!!.getString(routing_arg), "") + et_routing_content.text = Utils.getEditable(content!!) + + setHasOptionsMenu(true) + } + + override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { + inflater.inflate(R.menu.menu_routing, menu) + return super.onCreateOptionsMenu(menu, inflater) + } + + override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) { + R.id.save_routing -> { + val content = et_routing_content.text.toString() + activity?.defaultDPreference?.setPrefString(arguments!!.getString(routing_arg), content) + activity?.toast(R.string.toast_success) + true + } + R.id.del_routing -> { + et_routing_content.text = null + true + } + R.id.scan_replace -> { + scanQRcode(REQUEST_SCAN_REPLACE) + true + } + R.id.scan_append -> { + scanQRcode(REQUEST_SCAN_APPEND) + true + } + R.id.default_rules -> { + setDefaultRules() + true + } + else -> super.onOptionsItemSelected(item) + } + + fun scanQRcode(requestCode: Int): 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!!) + .request(Manifest.permission.CAMERA) + .subscribe { + if (it) + startActivityForResult(requestCode) + else + activity?.toast(R.string.toast_permission_denied) + } +// } + return true + } + + fun setDefaultRules(): Boolean { + var url = AppConfig.v2rayCustomRoutingListUrl + when (arguments!!.getString(routing_arg)) { + AppConfig.PREF_V2RAY_ROUTING_AGENT -> { + url += AppConfig.TAG_AGENT + } + AppConfig.PREF_V2RAY_ROUTING_DIRECT -> { + url += AppConfig.TAG_DIRECT + } + AppConfig.PREF_V2RAY_ROUTING_BLOCKED -> { + url += AppConfig.TAG_BLOCKED + } + } + + toast(R.string.msg_downloading_content) + doAsync { + val content = URL(url).readText() + uiThread { + et_routing_content.text = Utils.getEditable(content!!) + toast(R.string.toast_success) + } + } + 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") + } + } + } + + +} diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/ScScannerActivity.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/ScScannerActivity.kt new file mode 100644 index 000000000..b3cd04bba --- /dev/null +++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/ScScannerActivity.kt @@ -0,0 +1,52 @@ +package com.v2ray.ang.ui + +import android.Manifest +import android.content.* +import com.tbruyelle.rxpermissions.RxPermissions +import com.v2ray.ang.R +import com.v2ray.ang.util.AngConfigManager +import android.os.Bundle +import org.jetbrains.anko.* + +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) + } + + fun importQRcode(requestCode: Int): Boolean { + RxPermissions(this) + .request(Manifest.permission.CAMERA) + .subscribe { + if (it) + startActivityForResult(requestCode) + else + toast(R.string.toast_permission_denied) + } + + 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() + } + } + finish() + } + +} \ No newline at end of file diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/ScSwitchActivity.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/ScSwitchActivity.kt new file mode 100644 index 000000000..5b0230556 --- /dev/null +++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/ScSwitchActivity.kt @@ -0,0 +1,104 @@ +package com.v2ray.ang.ui + +import android.content.* +import android.net.VpnService +import com.v2ray.ang.R +import com.v2ray.ang.util.Utils +import android.os.Bundle +import com.v2ray.ang.AppConfig +import com.v2ray.ang.util.MessageUtil +import java.lang.ref.SoftReference +import android.content.IntentFilter +import kotlinx.android.synthetic.main.activity_main.* +import rx.Observable +import rx.android.schedulers.AndroidSchedulers +import java.util.concurrent.TimeUnit + +class ScSwitchActivity : BaseActivity() { + companion object { + private const val REQUEST_CODE_VPN_PREPARE = 0 + } + + var isRunning = false + set(value) { + field = value + if (value) { + Utils.stopVService(this) + } else { + val intent = VpnService.prepare(this) + if (intent == null) { + Utils.startVService(this) + } else { + startActivityForResult(intent, REQUEST_CODE_VPN_PREPARE) + } + } + finishActivity() + } + + fun finishActivity() { + try { + Observable.timer(5000, TimeUnit.MILLISECONDS) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe { + finish() + } + } catch (e: Exception) { + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + moveTaskToBack(true) + + setContentView(R.layout.activity_none) + + val isRunning = Utils.isServiceRun(this, "com.v2ray.ang.service.V2RayVpnService") + if (isRunning) { + //Utils.stopVService(this) + mMsgReceive = ReceiveMessageHandler(this@ScSwitchActivity) + registerReceiver(mMsgReceive, IntentFilter(AppConfig.BROADCAST_ACTION_ACTIVITY)) + MessageUtil.sendMsg2Service(this, AppConfig.MSG_REGISTER_CLIENT, "") + + } else { + Utils.startVService(this) + finishActivity() + } + } + + override fun onStop() { + super.onStop() + if (mMsgReceive != null) { + unregisterReceiver(mMsgReceive) + mMsgReceive = null + } + } + + private var mMsgReceive: BroadcastReceiver? = null + + private class ReceiveMessageHandler(activity: ScSwitchActivity) : BroadcastReceiver() { + internal var mReference: SoftReference = SoftReference(activity) + override fun onReceive(ctx: Context?, intent: Intent?) { + val activity = mReference.get() + when (intent?.getIntExtra("key", 0)) { + AppConfig.MSG_STATE_RUNNING -> { + activity?.isRunning = true + } + AppConfig.MSG_STATE_NOT_RUNNING -> { + activity?.isRunning = false + } +// AppConfig.MSG_STATE_START_SUCCESS -> { +// activity?.toast(R.string.toast_services_success) +// activity?.isRunning = true +// } +// AppConfig.MSG_STATE_START_FAILURE -> { +// activity?.toast(R.string.toast_services_failure) +// activity?.isRunning = false +// } +// AppConfig.MSG_STATE_STOP_SUCCESS -> { +// activity?.isRunning = false +// } + } + } + } + +} \ No newline at end of file diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/ScannerActivity.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/ScannerActivity.kt new file mode 100644 index 000000000..5e019309c --- /dev/null +++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/ScannerActivity.kt @@ -0,0 +1,136 @@ +package com.v2ray.ang.ui + +import android.Manifest +import android.app.Activity +import android.os.Bundle +import com.google.zxing.Result +import me.dm7.barcodescanner.zxing.ZXingScannerView +import android.content.Intent +import android.graphics.BitmapFactory +import android.icu.util.TimeUnit +import android.view.Menu +import android.view.MenuItem +import com.google.zxing.BarcodeFormat +import com.tbruyelle.rxpermissions.RxPermissions +import com.v2ray.ang.R +import com.v2ray.ang.util.QRCodeDecoder +import org.jetbrains.anko.toast +import rx.Observable +import android.os.SystemClock +import kotlinx.android.synthetic.main.activity_main.* +import rx.Observer +import rx.android.schedulers.AndroidSchedulers +import javax.xml.datatype.DatatypeConstants.SECONDS + + + + +class ScannerActivity : BaseActivity(), ZXingScannerView.ResultHandler { + companion object { + private const val REQUEST_FILE_CHOOSER = 2 + } + + + private var mScannerView: ZXingScannerView? = null + + public override fun onCreate(state: Bundle?) { + super.onCreate(state) + mScannerView = ZXingScannerView(this) // Programmatically initialize the scanner view + + mScannerView?.setAutoFocus(true) + val formats = ArrayList() + formats.add(BarcodeFormat.QR_CODE) + mScannerView?.setFormats(formats) + + setContentView(mScannerView) // Set the scanner view as the content view + + supportActionBar?.setDisplayHomeAsUpEnabled(true) + } + + public override fun onResume() { + super.onResume() + mScannerView!!.setResultHandler(this) // Register ourselves as a handler for scan results. + mScannerView!!.startCamera() // Start camera on resume + } + + public override fun onPause() { + super.onPause() + mScannerView!!.stopCamera() // Stop camera on pause + } + + override fun handleResult(rawResult: Result) { + // Do something with the result here +// Log.v(FragmentActivity.TAG, rawResult.text) // Prints scan results +// Log.v(FragmentActivity.TAG, rawResult.barcodeFormat.toString()) // Prints the scan format (qrcode, pdf417 etc.) + + finished(rawResult.text) + + // If you would like to resume scanning, call this method below: +// mScannerView!!.resumeCameraPreview(this) + } + + fun finished(text: String) { + val intent = Intent() + intent.putExtra("SCAN_RESULT", text) + setResult(Activity.RESULT_OK, intent) + finish() + } + + override fun onCreateOptionsMenu(menu: Menu?): Boolean { + menuInflater.inflate(R.menu.menu_scanner, menu) + return true + } + + override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) { + R.id.select_photo -> { + RxPermissions(this) + .request(Manifest.permission.READ_EXTERNAL_STORAGE) + .subscribe { + if (it) { + try { + showFileChooser() + } catch (e: Exception) { + e.printStackTrace() + } + } else + toast(R.string.toast_permission_denied) + } + true + } + else -> super.onOptionsItemSelected(item) + } + + private fun showFileChooser() { + val intent = Intent(Intent.ACTION_GET_CONTENT) + intent.type = "image/*" + intent.addCategory(Intent.CATEGORY_OPENABLE) + //intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true) + + try { + startActivityForResult( + Intent.createChooser(intent, getString(R.string.title_file_chooser)), + REQUEST_FILE_CHOOSER) + } catch (ex: android.content.ActivityNotFoundException) { + toast(R.string.toast_require_file_manager) + } + } + + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + when (requestCode) { + REQUEST_FILE_CHOOSER -> + if (resultCode == RESULT_OK) { + try { + val uri = data!!.data + val bitmap = BitmapFactory.decodeStream(contentResolver.openInputStream(uri)) + val text = QRCodeDecoder.syncDecodeQRCode(bitmap) + finished(text) + } catch (e: Exception) { + e.printStackTrace() + toast(e.message.toString()) + } + } + } + } +} diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/Server2Activity.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/Server2Activity.kt new file mode 100644 index 000000000..4fef0cf75 --- /dev/null +++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/Server2Activity.kt @@ -0,0 +1,139 @@ +package com.v2ray.ang.ui + +import android.os.Bundle +import android.text.TextUtils +import android.view.Menu +import android.view.MenuItem +import com.v2ray.ang.AppConfig +import com.v2ray.ang.R +import com.v2ray.ang.extension.defaultDPreference +import com.v2ray.ang.dto.AngConfig +import com.v2ray.ang.util.AngConfigManager +import com.v2ray.ang.util.Utils +import kotlinx.android.synthetic.main.activity_server2.* +import org.jetbrains.anko.* + + +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 = 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 { + val vmess = configs.vmess[edit_index] + + vmess.remarks = et_remarks.text.toString() + + if (TextUtils.isEmpty(vmess.remarks)) { + toast(R.string.server_lab_remarks) + return false + } + + if (AngConfigManager.addCustomServer(vmess, edit_index) == 0) { + toast(R.string.toast_success) + finish() + return true + } else { + toast(R.string.toast_failure) + return false + } + } + + /** + * save server config + */ + fun deleteServer(): Boolean { + if (edit_index >= 0) { + alert(R.string.del_config_comfirm) { + positiveButton(android.R.string.ok) { + if (AngConfigManager.removeServer(edit_index) == 0) { + toast(R.string.toast_success) + finish() + } else { + toast(R.string.toast_failure) + } + } + show() + } + } else { + } + return true + } + + override fun onCreateOptionsMenu(menu: Menu?): Boolean { + menuInflater.inflate(R.menu.action_server, menu) + del_config = menu?.findItem(R.id.del_config) + save_config = menu?.findItem(R.id.save_config) + + if (edit_index >= 0) { + if (isRunning) { + if (edit_index == configs.index) { + del_config?.isVisible = false + save_config?.isVisible = false + } + } + } else { + del_config?.isVisible = false + } + + return super.onCreateOptionsMenu(menu) + } + + override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) { + R.id.del_config -> { + deleteServer() + true + } + R.id.save_config -> { + saveServer() + true + } + else -> super.onOptionsItemSelected(item) + } +} \ No newline at end of file diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/Server3Activity.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/Server3Activity.kt new file mode 100644 index 000000000..92ee6e2f7 --- /dev/null +++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/Server3Activity.kt @@ -0,0 +1,175 @@ +package com.v2ray.ang.ui + +import android.os.Bundle +import android.text.TextUtils +import android.view.Menu +import android.view.MenuItem +import com.v2ray.ang.R +import com.v2ray.ang.dto.AngConfig +import com.v2ray.ang.util.AngConfigManager +import com.v2ray.ang.util.Utils +import kotlinx.android.synthetic.main.activity_server3.* +import org.jetbrains.anko.* + + +class Server3Activity : BaseActivity() { + companion object { + private const val REQUEST_SCAN = 1 + } + + var del_config: MenuItem? = null + var save_config: MenuItem? = null + + private lateinit var configs: AngConfig + private var edit_index: Int = -1 //当前编辑的服务器 + private var edit_guid: String = "" + private var isRunning: Boolean = false + private val securitys: Array by lazy { + resources.getStringArray(R.array.ss_securitys) + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_server3) + + configs = AngConfigManager.configs + edit_index = intent.getIntExtra("position", -1) + isRunning = intent.getBooleanExtra("isRunning", false) + title = getString(R.string.title_server) + + if (edit_index >= 0) { + edit_guid = configs.vmess[edit_index].guid + bindingServer(configs.vmess[edit_index]) + } else { + clearServer() + } + supportActionBar?.setDisplayHomeAsUpEnabled(true) + } + + /** + * bingding seleced server config + */ + fun bindingServer(vmess: AngConfig.VmessBean): Boolean { + et_remarks.text = Utils.getEditable(vmess.remarks) + + et_address.text = Utils.getEditable(vmess.address) + et_port.text = Utils.getEditable(vmess.port.toString()) + et_id.text = Utils.getEditable(vmess.id) + val security = Utils.arrayFind(securitys, vmess.security) + if (security >= 0) { + sp_security.setSelection(security) + } + + return true + } + + /** + * clear or init server config + */ + fun clearServer(): Boolean { + et_remarks.text = null + et_address.text = null + et_port.text = Utils.getEditable("10086") + et_id.text = null + sp_security.setSelection(0) + + return true + } + + /** + * save server config + */ + fun saveServer(): Boolean { + val vmess: AngConfig.VmessBean + if (edit_index >= 0) { + vmess = configs.vmess[edit_index] + } else { + vmess = AngConfig.VmessBean() + } + + vmess.guid = edit_guid + vmess.remarks = et_remarks.text.toString() + vmess.address = et_address.text.toString() + vmess.port = Utils.parseInt(et_port.text.toString()) + vmess.id = et_id.text.toString() + vmess.security = securitys[sp_security.selectedItemPosition] + + if (TextUtils.isEmpty(vmess.remarks)) { + toast(R.string.server_lab_remarks) + return false + } + if (TextUtils.isEmpty(vmess.address)) { + toast(R.string.server_lab_address3) + return false + } + if (TextUtils.isEmpty(vmess.port.toString()) || vmess.port <= 0) { + toast(R.string.server_lab_port3) + return false + } + if (TextUtils.isEmpty(vmess.id)) { + toast(R.string.server_lab_id3) + return false + } + + if (AngConfigManager.addShadowsocksServer(vmess, edit_index) == 0) { + toast(R.string.toast_success) + finish() + return true + } else { + toast(R.string.toast_failure) + return false + } + } + + /** + * save server config + */ + fun deleteServer(): Boolean { + if (edit_index >= 0) { + alert(R.string.del_config_comfirm) { + positiveButton(android.R.string.ok) { + if (AngConfigManager.removeServer(edit_index) == 0) { + toast(R.string.toast_success) + finish() + } else { + toast(R.string.toast_failure) + } + } + show() + } + } else { + } + return true + } + + override fun onCreateOptionsMenu(menu: Menu?): Boolean { + menuInflater.inflate(R.menu.action_server, menu) + del_config = menu?.findItem(R.id.del_config) + save_config = menu?.findItem(R.id.save_config) + + if (edit_index >= 0) { + if (isRunning) { + if (edit_index == configs.index) { + del_config?.isVisible = false + save_config?.isVisible = false + } + } + } else { + del_config?.isVisible = false + } + + return super.onCreateOptionsMenu(menu) + } + + override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) { + R.id.del_config -> { + deleteServer() + true + } + R.id.save_config -> { + saveServer() + true + } + else -> super.onOptionsItemSelected(item) + } +} \ No newline at end of file diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/Server4Activity.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/Server4Activity.kt new file mode 100644 index 000000000..97fb4d63a --- /dev/null +++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/Server4Activity.kt @@ -0,0 +1,159 @@ +package com.v2ray.ang.ui + +import android.os.Bundle +import android.text.TextUtils +import android.view.Menu +import android.view.MenuItem +import com.v2ray.ang.R +import com.v2ray.ang.dto.AngConfig +import com.v2ray.ang.util.AngConfigManager +import com.v2ray.ang.util.Utils +import kotlinx.android.synthetic.main.activity_server4.* +import org.jetbrains.anko.* + + +class Server4Activity : BaseActivity() { + companion object { + private const val REQUEST_SCAN = 1 + } + + var del_config: MenuItem? = null + var save_config: MenuItem? = null + + private lateinit var configs: AngConfig + private var edit_index: Int = -1 //当前编辑的服务器 + private var edit_guid: String = "" + private var isRunning: Boolean = false + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_server4) + + configs = AngConfigManager.configs + edit_index = intent.getIntExtra("position", -1) + isRunning = intent.getBooleanExtra("isRunning", false) + title = getString(R.string.title_server) + + if (edit_index >= 0) { + edit_guid = configs.vmess[edit_index].guid + bindingServer(configs.vmess[edit_index]) + } else { + clearServer() + } + supportActionBar?.setDisplayHomeAsUpEnabled(true) + } + + /** + * bingding seleced server config + */ + fun bindingServer(vmess: AngConfig.VmessBean): Boolean { + et_remarks.text = Utils.getEditable(vmess.remarks) + + et_address.text = Utils.getEditable(vmess.address) + et_port.text = Utils.getEditable(vmess.port.toString()) + + return true + } + + /** + * clear or init server config + */ + fun clearServer(): Boolean { + et_remarks.text = null + et_address.text = null + et_port.text = Utils.getEditable("10086") + + return true + } + + /** + * save server config + */ + fun saveServer(): Boolean { + val vmess: AngConfig.VmessBean + if (edit_index >= 0) { + vmess = configs.vmess[edit_index] + } else { + vmess = AngConfig.VmessBean() + } + + vmess.guid = edit_guid + vmess.remarks = et_remarks.text.toString() + vmess.address = et_address.text.toString() + vmess.port = Utils.parseInt(et_port.text.toString()) + + if (TextUtils.isEmpty(vmess.remarks)) { + toast(R.string.server_lab_remarks) + return false + } + if (TextUtils.isEmpty(vmess.address)) { + toast(R.string.server_lab_address3) + return false + } + if (TextUtils.isEmpty(vmess.port.toString()) || vmess.port <= 0) { + toast(R.string.server_lab_port3) + return false + } + + if (AngConfigManager.addSocksServer(vmess, edit_index) == 0) { + toast(R.string.toast_success) + finish() + return true + } else { + toast(R.string.toast_failure) + return false + } + } + + /** + * save server config + */ + fun deleteServer(): Boolean { + if (edit_index >= 0) { + alert(R.string.del_config_comfirm) { + positiveButton(android.R.string.ok) { + if (AngConfigManager.removeServer(edit_index) == 0) { + toast(R.string.toast_success) + finish() + } else { + toast(R.string.toast_failure) + } + } + show() + } + } else { + } + return true + } + + override fun onCreateOptionsMenu(menu: Menu?): Boolean { + menuInflater.inflate(R.menu.action_server, menu) + del_config = menu?.findItem(R.id.del_config) + save_config = menu?.findItem(R.id.save_config) + + if (edit_index >= 0) { + if (isRunning) { + if (edit_index == configs.index) { + del_config?.isVisible = false + save_config?.isVisible = false + } + } + } else { + del_config?.isVisible = false + } + + return super.onCreateOptionsMenu(menu) + } + + override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) { + R.id.del_config -> { + deleteServer() + true + } + R.id.save_config -> { + saveServer() + true + } + else -> super.onOptionsItemSelected(item) + } +} \ No newline at end of file diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/ServerActivity.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/ServerActivity.kt new file mode 100644 index 000000000..d2a140352 --- /dev/null +++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/ServerActivity.kt @@ -0,0 +1,218 @@ +package com.v2ray.ang.ui + +import android.os.Bundle +import android.text.TextUtils +import android.view.Menu +import android.view.MenuItem +import com.v2ray.ang.R +import com.v2ray.ang.dto.AngConfig +import com.v2ray.ang.util.AngConfigManager +import com.v2ray.ang.util.Utils +import kotlinx.android.synthetic.main.activity_server.* +import org.jetbrains.anko.* + + +class ServerActivity : 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 by lazy { + resources.getStringArray(R.array.securitys) + } + private val networks: Array by lazy { + resources.getStringArray(R.array.networks) + } + private val headertypes: Array by lazy { + resources.getStringArray(R.array.headertypes) + } + private val streamsecuritys: Array by lazy { + resources.getStringArray(R.array.streamsecuritys) + } + + 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]) + } 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) + et_alterId.text = Utils.getEditable(vmess.alterId.toString()) + + val security = Utils.arrayFind(securitys, vmess.security) + if (security >= 0) { + sp_security.setSelection(security) + } + val network = Utils.arrayFind(networks, vmess.network) + if (network >= 0) { + sp_network.setSelection(network) + } + + val headerType = Utils.arrayFind(headertypes, vmess.headerType) + if (headerType >= 0) { + sp_header_type.setSelection(headerType) + } + et_request_host.text = Utils.getEditable(vmess.requestHost) + et_path.text = Utils.getEditable(vmess.path) + + val streamSecurity = Utils.arrayFind(streamsecuritys, vmess.streamSecurity) + if (streamSecurity >= 0) { + sp_stream_security.setSelection(streamSecurity) + } + 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 + et_alterId.text = Utils.getEditable("64") + 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) + 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)) { + toast(R.string.server_lab_remarks) + return false + } + if (TextUtils.isEmpty(vmess.address)) { + toast(R.string.server_lab_address) + return false + } + if (TextUtils.isEmpty(vmess.port.toString()) || vmess.port <= 0) { + toast(R.string.server_lab_port) + return false + } + if (TextUtils.isEmpty(vmess.id)) { + 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 + } + + if (AngConfigManager.addServer(vmess, edit_index) == 0) { + toast(R.string.toast_success) + finish() + return true + } else { + toast(R.string.toast_failure) + return false + } + } + + /** + * save server config + */ + fun deleteServer(): Boolean { + if (edit_index >= 0) { + alert(R.string.del_config_comfirm) { + positiveButton(android.R.string.ok) { + if (AngConfigManager.removeServer(edit_index) == 0) { + toast(R.string.toast_success) + finish() + } else { + toast(R.string.toast_failure) + } + } + show() + } + } else { + } + return true + } + + override fun onCreateOptionsMenu(menu: Menu?): Boolean { + menuInflater.inflate(R.menu.action_server, menu) + del_config = menu?.findItem(R.id.del_config) + save_config = menu?.findItem(R.id.save_config) + + if (edit_index >= 0) { + if (isRunning) { + if (edit_index == configs.index) { + del_config?.isVisible = false + save_config?.isVisible = false + } + } + } else { + del_config?.isVisible = false + } + + return super.onCreateOptionsMenu(menu) + } + + override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) { + R.id.del_config -> { + deleteServer() + true + } + R.id.save_config -> { + saveServer() + true + } + else -> super.onOptionsItemSelected(item) + } +} \ No newline at end of file diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/SettingsActivity.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/SettingsActivity.kt new file mode 100644 index 000000000..583ae76a9 --- /dev/null +++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/SettingsActivity.kt @@ -0,0 +1,180 @@ +package com.v2ray.ang.ui + +import android.content.Intent +import android.content.SharedPreferences +import android.net.Uri +import android.os.Bundle +import android.preference.CheckBoxPreference +import android.preference.EditTextPreference +import android.preference.Preference +import android.preference.PreferenceFragment +import com.v2ray.ang.BuildConfig +//import com.v2ray.ang.InappBuyActivity +import com.v2ray.ang.R +import com.v2ray.ang.AppConfig +import com.v2ray.ang.extension.defaultDPreference +import com.v2ray.ang.extension.onClick +import com.v2ray.ang.util.Utils +import org.jetbrains.anko.act +import org.jetbrains.anko.defaultSharedPreferences +import org.jetbrains.anko.startActivity +import org.jetbrains.anko.toast +import libv2ray.Libv2ray + +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_LOCAL_DNS_ENABLED = "pref_local_dns_enabled" + const val PREF_REMOTE_DNS = "pref_remote_dns" + const val PREF_DOMESTIC_DNS = "pref_domestic_dns" + +// const val PREF_SOCKS_PORT = "pref_socks_port" +// const val PREF_HTTP_PORT = "pref_http_port" + + const val PREF_ROUTING_DOMAIN_STRATEGY = "pref_routing_domain_strategy" + const val PREF_ROUTING_MODE = "pref_routing_mode" + const val PREF_ROUTING_CUSTOM = "pref_routing_custom" +// const val PREF_DONATE = "pref_donate" + // const val PREF_LICENSES = "pref_licenses" +// const val PREF_FEEDBACK = "pref_feedback" +// const val PREF_TG_GROUP = "pref_tg_group" + const val PREF_VERSION = "pref_version" + // const val PREF_AUTO_RESTART = "pref_auto_restart" + const val PREF_FORWARD_IPV6 = "pref_forward_ipv6" + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_settings) + + title = getString(R.string.title_settings) + + supportActionBar?.setDisplayHomeAsUpEnabled(true) + } + + class SettingsFragment : PreferenceFragment(), SharedPreferences.OnSharedPreferenceChangeListener { + val perAppProxy by lazy { findPreference(PREF_PER_APP_PROXY) as CheckBoxPreference } + // val autoRestart by lazy { findPreference(PREF_AUTO_RESTART) as CheckBoxPreference } + val remoteDns by lazy { findPreference(PREF_REMOTE_DNS) as EditTextPreference } + val domesticDns by lazy { findPreference(PREF_DOMESTIC_DNS) as EditTextPreference } + + val enableLocalDns by lazy { findPreference(PREF_LOCAL_DNS_ENABLED) as CheckBoxPreference } + val forwardIpv6 by lazy { findPreference(PREF_FORWARD_IPV6) as CheckBoxPreference } + +// val socksPort by lazy { findPreference(PREF_SOCKS_PORT) as EditTextPreference } +// val httpPort by lazy { findPreference(PREF_HTTP_PORT) as EditTextPreference } + + val routingCustom: Preference by lazy { findPreference(PREF_ROUTING_CUSTOM) } +// val donate: Preference by lazy { findPreference(PREF_DONATE) } + // val licenses: Preference by lazy { findPreference(PREF_LICENSES) } +// val feedback: Preference by lazy { findPreference(PREF_FEEDBACK) } +// val tgGroup: Preference by lazy { findPreference(PREF_TG_GROUP) } + val version: Preference by lazy { findPreference(PREF_VERSION) } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + addPreferencesFromResource(R.xml.pref_settings) + + routingCustom.onClick { + startActivity() + } + +// donate.onClick { +// startActivity() +// } + +// licenses.onClick { +// val fragment = LicensesDialogFragment.Builder(act) +// .setNotices(R.raw.licenses) +// .setIncludeOwnLicense(false) +// .build() +// fragment.show((act as AppCompatActivity).supportFragmentManager, null) +// } +// +// feedback.onClick { +// Utils.openUri(activity, "https://github.com/2dust/v2rayNG/issues") +// } +// tgGroup.onClick { +// // Utils.openUri(activity, "https://t.me/v2rayN") +// val intent = Intent(Intent.ACTION_VIEW, Uri.parse("tg:resolve?domain=v2rayN")) +// try { +// startActivity(intent) +// } catch (e: Exception) { +// e.printStackTrace() +// toast(R.string.toast_tg_app_not_found) +// } +// } + + perAppProxy.setOnPreferenceClickListener { + startActivity() + perAppProxy.isChecked = true + false + } + + remoteDns.setOnPreferenceChangeListener { preference, any -> + // remoteDns.summary = any as String + val nval = any as String + remoteDns.summary = if (nval == "") AppConfig.DNS_AGENT else nval + true + } + domesticDns.setOnPreferenceChangeListener { preference, any -> + // domesticDns.summary = any as String + val nval = any as String + domesticDns.summary = if (nval == "") AppConfig.DNS_DIRECT else nval + true + } +// socksPort.setOnPreferenceChangeListener { preference, any -> +// socksPort.summary = any as String +// true +// } +// httpPort.setOnPreferenceChangeListener { preference, any -> +// httpPort.summary = any as String +// true +// } + + version.summary = "${BuildConfig.VERSION_NAME} (${Libv2ray.checkVersionX()})" + } + + override fun onStart() { + super.onStart() + + perAppProxy.isChecked = defaultSharedPreferences.getBoolean(PREF_PER_APP_PROXY, false) + remoteDns.summary = defaultSharedPreferences.getString(PREF_REMOTE_DNS, "") + domesticDns.summary = defaultSharedPreferences.getString(PREF_DOMESTIC_DNS, "") + + if (remoteDns.summary == "") { + remoteDns.summary = AppConfig.DNS_AGENT + } + + if ( domesticDns.summary == "") { + domesticDns.summary = AppConfig.DNS_DIRECT + } + +// socksPort.summary = defaultSharedPreferences.getString(PREF_SOCKS_PORT, "10808") +// lanconnPort.summary = defaultSharedPreferences.getString(PREF_HTTP_PORT, "") + + defaultSharedPreferences.registerOnSharedPreferenceChangeListener(this) + } + + override fun onStop() { + super.onStop() + defaultSharedPreferences.unregisterOnSharedPreferenceChangeListener(this) + } + + override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String?) { + when (key) { +// PREF_AUTO_RESTART -> +// act.defaultDPreference.setPrefBoolean(key, sharedPreferences.getBoolean(key, false)) + + PREF_PER_APP_PROXY -> + act.defaultDPreference.setPrefBoolean(key, sharedPreferences.getBoolean(key, false)) + } + } + } + +} \ No newline at end of file diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/SubEditActivity.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/SubEditActivity.kt new file mode 100644 index 000000000..d8cb7d2e2 --- /dev/null +++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/SubEditActivity.kt @@ -0,0 +1,138 @@ +package com.v2ray.ang.ui + +import android.os.Bundle +import android.text.TextUtils +import android.view.Menu +import android.view.MenuItem +import com.v2ray.ang.R +import com.v2ray.ang.dto.AngConfig +import com.v2ray.ang.util.AngConfigManager +import com.v2ray.ang.util.Utils +import kotlinx.android.synthetic.main.activity_sub_edit.* +import org.jetbrains.anko.* + + +class SubEditActivity : BaseActivity() { + + var del_config: MenuItem? = null + var save_config: MenuItem? = null + + private lateinit var configs: AngConfig + private var edit_index: Int = -1 //当前编辑的 + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_sub_edit) + + configs = AngConfigManager.configs + edit_index = intent.getIntExtra("position", -1) + + title = getString(R.string.title_sub_setting) + + if (edit_index >= 0) { + bindingServer(configs.subItem[edit_index]) + } else { + clearServer() + } + supportActionBar?.setDisplayHomeAsUpEnabled(true) + } + + /** + * bingding seleced server config + */ + fun bindingServer(subItem: AngConfig.SubItemBean): Boolean { + et_remarks.text = Utils.getEditable(subItem.remarks) + et_url.text = Utils.getEditable(subItem.url) + + return true + } + + /** + * clear or init server config + */ + fun clearServer(): Boolean { + et_remarks.text = null + et_url.text = null + + return true + } + + /** + * save server config + */ + fun saveServer(): Boolean { + val subItem: AngConfig.SubItemBean + if (edit_index >= 0) { + subItem = configs.subItem[edit_index] + } else { + subItem = AngConfig.SubItemBean() + } + + subItem.remarks = et_remarks.text.toString() + subItem.url = et_url.text.toString() + + 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 (AngConfigManager.addSubItem(subItem, edit_index) == 0) { + toast(R.string.toast_success) + finish() + return true + } else { + toast(R.string.toast_failure) + return false + } + } + + /** + * save server config + */ + fun deleteServer(): Boolean { + if (edit_index >= 0) { + alert(R.string.del_config_comfirm) { + positiveButton(android.R.string.ok) { + if (AngConfigManager.removeSubItem(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) { + } 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) + } +} \ No newline at end of file diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/SubSettingActivity.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/SubSettingActivity.kt new file mode 100644 index 000000000..709ef38b8 --- /dev/null +++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/SubSettingActivity.kt @@ -0,0 +1,51 @@ +package com.v2ray.ang.ui + +import android.support.v7.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 org.jetbrains.anko.startActivity + +class SubSettingActivity : BaseActivity() { + + private val adapter by lazy { SubSettingRecyclerAdapter(this) } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_sub_setting) + + title = getString(R.string.title_sub_setting) + + recycler_view.setHasFixedSize(true) + recycler_view.layoutManager = LinearLayoutManager(this) + recycler_view.adapter = adapter + + supportActionBar?.setDisplayHomeAsUpEnabled(true) + } + + override fun onResume() { + super.onResume() + adapter.updateConfigList() + } + + override fun onCreateOptionsMenu(menu: Menu?): Boolean { + menuInflater.inflate(R.menu.action_sub_setting, menu) + menu?.findItem(R.id.del_config)?.isVisible = false + menu?.findItem(R.id.save_config)?.isVisible = false + + return super.onCreateOptionsMenu(menu) + } + + override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) { + R.id.add_config -> { + startActivity("position" to -1) + adapter.updateConfigList() + true + } + else -> super.onOptionsItemSelected(item) + } + + +} \ No newline at end of file diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/SubSettingRecyclerAdapter.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/SubSettingRecyclerAdapter.kt new file mode 100644 index 000000000..f8cd88248 --- /dev/null +++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/SubSettingRecyclerAdapter.kt @@ -0,0 +1,62 @@ +package com.v2ray.ang.ui + +import android.graphics.Color +import android.support.v7.widget.RecyclerView +import android.view.View +import android.view.ViewGroup +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 org.jetbrains.anko.* + +class SubSettingRecyclerAdapter(val activity: SubSettingActivity) : RecyclerView.Adapter() { + + private var mActivity: SubSettingActivity = activity + private lateinit var configs: AngConfig + + init { + updateConfigList() + } + + 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.backgroundColor = Color.TRANSPARENT + + holder.layout_edit.setOnClickListener { + mActivity.startActivity("position" to position) + } + } else { + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseViewHolder { + return MainViewHolder(parent.context.layoutInflater + .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!! + } + +} diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/TaskerActivity.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/TaskerActivity.kt new file mode 100644 index 000000000..fac81876c --- /dev/null +++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/TaskerActivity.kt @@ -0,0 +1,111 @@ +package com.v2ray.ang.ui + +import android.app.Activity +import android.os.Bundle +import android.view.View +import android.widget.ArrayAdapter +import android.widget.ListView +import java.util.ArrayList +import com.v2ray.ang.R +import 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.v2ray.ang.AppConfig +import kotlinx.android.synthetic.main.activity_tasker.* + + +class TaskerActivity : BaseActivity() { + private var listview: ListView? = null + private var lstData: ArrayList = ArrayList() + private var lstGuid: ArrayList = ArrayList() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_tasker) + + //add def value + lstData.add("Default") + lstGuid.add(AppConfig.TASKER_DEFAULT_GUID) + + AngConfigManager.configs.vmess.forEach { + lstData.add(it.remarks) + lstGuid.add(it.guid) + } + val adapter = ArrayAdapter(this, + android.R.layout.simple_list_item_single_choice, lstData) + listview = findViewById(R.id.listview) as ListView + listview!!.adapter = adapter + + init() + } + + private fun init() { + try { + val bundle = intent?.getBundleExtra(AppConfig.TASKER_EXTRA_BUNDLE) + val switch = bundle?.getBoolean(AppConfig.TASKER_EXTRA_BUNDLE_SWITCH, false) + val guid = bundle?.getString(AppConfig.TASKER_EXTRA_BUNDLE_GUID, "") + + if (switch == null || TextUtils.isEmpty(guid)) { + return + } else { + switch_start_service.isChecked = switch + val pos = lstGuid.indexOf(guid.toString()) + if (pos >= 0) { + listview?.setItemChecked(pos, true) + } + } + } catch (e: WriterException) { + e.printStackTrace() + + } + } + + private fun confirmFinish() { + val position = listview?.checkedItemPosition + if (position == null || position < 0) { + return + } + + val extraBundle = Bundle() + extraBundle.putBoolean(AppConfig.TASKER_EXTRA_BUNDLE_SWITCH, switch_start_service.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" + } else { + blurb = "Stop $remarks" + } + + intent.putExtra(AppConfig.TASKER_EXTRA_BUNDLE, extraBundle) + intent.putExtra(AppConfig.TASKER_EXTRA_STRING_BLURB, blurb) + setResult(Activity.RESULT_OK, intent) + finish() + } + + override fun onCreateOptionsMenu(menu: Menu?): Boolean { + menuInflater.inflate(R.menu.action_server, menu) + val del_config = menu?.findItem(R.id.del_config) + del_config?.isVisible = false + return super.onCreateOptionsMenu(menu) + } + + override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) { + R.id.del_config -> { + true + } + R.id.save_config -> { + confirmFinish() + true + } + else -> super.onOptionsItemSelected(item) + } + +} + diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/AngConfigManager.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/AngConfigManager.kt new file mode 100644 index 000000000..b7cef52cf --- /dev/null +++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/AngConfigManager.kt @@ -0,0 +1,824 @@ +package com.v2ray.ang.util + +import android.graphics.Bitmap +import android.text.TextUtils +import android.util.Log +import com.google.gson.Gson +import com.v2ray.ang.AngApplication +import com.v2ray.ang.AppConfig +import com.v2ray.ang.AppConfig.ANG_CONFIG +import com.v2ray.ang.AppConfig.PREF_CURR_CONFIG +import com.v2ray.ang.AppConfig.PREF_CURR_CONFIG_GUID +import com.v2ray.ang.AppConfig.PREF_CURR_CONFIG_NAME +import com.v2ray.ang.AppConfig.SOCKS_PROTOCOL +import com.v2ray.ang.AppConfig.SS_PROTOCOL +import com.v2ray.ang.AppConfig.VMESS_PROTOCOL +import com.v2ray.ang.R +import com.v2ray.ang.dto.AngConfig +import com.v2ray.ang.dto.VmessQRCode +import com.v2ray.ang.extension.defaultDPreference +import org.jetbrains.anko.toast +import java.net.URLDecoder +import java.util.* +import java.net.* +import java.math.BigInteger + +object AngConfigManager { + private lateinit var app: AngApplication + private lateinit var angConfig: AngConfig + val configs: AngConfig get() = angConfig + + fun inject(app: AngApplication) { + this.app = app + if (app.firstRun) { + } + loadConfig() + } + + /** + * loading config + */ + fun loadConfig() { + try { + val context = app.defaultDPreference.getPrefString(ANG_CONFIG, "") + if (!TextUtils.isEmpty(context)) { + angConfig = Gson().fromJson(context, AngConfig::class.java) + } else { + angConfig = AngConfig(0, vmess = arrayListOf(AngConfig.VmessBean()), subItem = arrayListOf(AngConfig.SubItemBean())) + angConfig.index = -1 + angConfig.vmess.clear() + angConfig.subItem.clear() + } + + for (i in angConfig.vmess.indices) { + upgradeServerVersion(angConfig.vmess[i]) + } + + if (configs.subItem == null) { + configs.subItem = arrayListOf(AngConfig.SubItemBean()) + } + + } catch (e: Exception) { + e.printStackTrace() + } + } + + /** + * add or edit server + */ + fun addServer(vmess: AngConfig.VmessBean, index: Int): Int { + try { + vmess.configVersion = 2 + vmess.configType = AppConfig.EConfigType.Vmess + + if (index >= 0) { + //edit + angConfig.vmess[index] = vmess + } else { + //add + vmess.guid = Utils.getUuid() + angConfig.vmess.add(vmess) + if (angConfig.vmess.count() == 1) { + angConfig.index = 0 + } + } + + storeConfigFile() + } catch (e: Exception) { + e.printStackTrace() + return -1 + } + return 0 + } + + /** + * 移除服务器 + */ + fun removeServer(index: Int): Int { + try { + if (index < 0 || index > angConfig.vmess.count() - 1) { + return -1 + } + + //删除 + angConfig.vmess.removeAt(index) + + //移除的是活动的 + if (angConfig.index == index) { + if (angConfig.vmess.count() > 0) { + angConfig.index = 0 + } else { + angConfig.index = -1 + } + } else if (index < angConfig.index)//移除活动之前的 + { + angConfig.index-- + } + + storeConfigFile() + } catch (e: Exception) { + e.printStackTrace() + return -1 + } + return 0 + } + + fun swapServer(fromPosition: Int, toPosition: Int): Int { + try { + Collections.swap(angConfig.vmess, fromPosition, toPosition) + + val index = angConfig.index + if (index == fromPosition) { + angConfig.index = toPosition + } else if (index == toPosition) { + angConfig.index = fromPosition + } + storeConfigFile() + } catch (e: Exception) { + e.printStackTrace() + return -1 + } + return 0 + } + + /** + * set active server + */ + fun setActiveServer(index: Int): Int { + try { + if (index < 0 || index > angConfig.vmess.count() - 1) { + return -1 + } + angConfig.index = index + + storeConfigFile() + } catch (e: Exception) { + e.printStackTrace() + return -1 + } + return 0 + } + + /** + * store config to file + */ + fun storeConfigFile() { + try { + val conf = Gson().toJson(angConfig) + app.defaultDPreference.setPrefString(ANG_CONFIG, conf) + } catch (e: Exception) { + e.printStackTrace() + } + } + + /** + * gen and store v2ray config file + */ + fun genStoreV2rayConfig(index: Int): Boolean { + try { + if (angConfig.index < 0 + || angConfig.vmess.count() <= 0 + || angConfig.index > angConfig.vmess.count() - 1 + ) { + return false + } + var index2 = angConfig.index + if (index >= 0) { + index2 = index + } + + val result = V2rayConfigUtil.getV2rayConfig(app, angConfig.vmess[index2]) + if (result.status) { + app.defaultDPreference.setPrefString(PREF_CURR_CONFIG, result.content) + app.defaultDPreference.setPrefString(PREF_CURR_CONFIG_GUID, currConfigGuid()) + app.defaultDPreference.setPrefString(PREF_CURR_CONFIG_NAME, currConfigName()) + return true + } else { + return false + } + } catch (e: Exception) { + e.printStackTrace() + return false + } + } + + fun currGeneratedV2rayConfig(): String { + return app.defaultDPreference.getPrefString(AppConfig.PREF_CURR_CONFIG, "") + } + + fun currConfigType(): Int { + if (angConfig.index < 0 + || angConfig.vmess.count() <= 0 + || angConfig.index > angConfig.vmess.count() - 1 + ) { + return -1 + } + return angConfig.vmess[angConfig.index].configType + } + + fun currConfigName(): String { + if (angConfig.index < 0 + || angConfig.vmess.count() <= 0 + || angConfig.index > angConfig.vmess.count() - 1 + ) { + return "" + } + return angConfig.vmess[angConfig.index].remarks + } + + fun currConfigGuid(): String { + if (angConfig.index < 0 + || angConfig.vmess.count() <= 0 + || angConfig.index > angConfig.vmess.count() - 1 + ) { + return "" + } + return angConfig.vmess[angConfig.index].guid + } + + /** + * import config form qrcode or... + */ + fun importConfig(server: String?, subid: String): Int { + try { + if (server == null || TextUtils.isEmpty(server)) { + return R.string.toast_none_data + } + + var vmess = AngConfig.VmessBean() + + if (server.startsWith(VMESS_PROTOCOL)) { + + val indexSplit = server.indexOf("?") + if (indexSplit > 0) { + vmess = ResolveVmess4Kitsunebi(server) + } else { + + var result = server.replace(VMESS_PROTOCOL, "") + result = Utils.decode(result) + if (TextUtils.isEmpty(result)) { + return R.string.toast_decoding_failed + } + val vmessQRCode = Gson().fromJson(result, VmessQRCode::class.java) + if (TextUtils.isEmpty(vmessQRCode.add) + || TextUtils.isEmpty(vmessQRCode.port) + || TextUtils.isEmpty(vmessQRCode.id) + || TextUtils.isEmpty(vmessQRCode.aid) + || TextUtils.isEmpty(vmessQRCode.net) + ) { + return R.string.toast_incorrect_protocol + } + + vmess.configType = AppConfig.EConfigType.Vmess + vmess.security = "auto" + vmess.network = "tcp" + vmess.headerType = "none" + + vmess.configVersion = Utils.parseInt(vmessQRCode.v) + vmess.remarks = vmessQRCode.ps + vmess.address = vmessQRCode.add + vmess.port = Utils.parseInt(vmessQRCode.port) + vmess.id = vmessQRCode.id + vmess.alterId = Utils.parseInt(vmessQRCode.aid) + vmess.network = vmessQRCode.net + vmess.headerType = vmessQRCode.type + vmess.requestHost = vmessQRCode.host + vmess.path = vmessQRCode.path + vmess.streamSecurity = vmessQRCode.tls + vmess.subid = subid + } + upgradeServerVersion(vmess) + addServer(vmess, -1) + + } else if (server.startsWith(SS_PROTOCOL)) { + var result = server.replace(SS_PROTOCOL, "") + val indexSplit = result.indexOf("#") + if (indexSplit > 0) { + try { + vmess.remarks = Utils.urlDecode(result.substring(indexSplit + 1, result.length)) + } catch (e: Exception) { + e.printStackTrace() + } + + result = result.substring(0, indexSplit) + } + + //part decode + val indexS = result.indexOf("@") + if (indexS > 0) { + result = Utils.decode(result.substring(0, indexS)) + result.substring(indexS, result.length) + } else { + result = Utils.decode(result) + } + + val legacyPattern = "^(.+?):(.*)@(.+?):(\\d+?)$".toRegex() + val match = legacyPattern.matchEntire(result) + if (match == null) { + return R.string.toast_incorrect_protocol + } + vmess.security = match.groupValues[1].toLowerCase() + vmess.id = match.groupValues[2] + vmess.address = match.groupValues[3] + if (vmess.address.firstOrNull() == '[' && vmess.address.lastOrNull() == ']') + vmess.address = vmess.address.substring(1, vmess.address.length - 1) + vmess.port = match.groupValues[4].toInt() + vmess.subid = subid + + addShadowsocksServer(vmess, -1) + } else if (server.startsWith(SOCKS_PROTOCOL)) { + var result = server.replace(SOCKS_PROTOCOL, "") + val indexSplit = result.indexOf("#") + if (indexSplit > 0) { + try { + vmess.remarks = Utils.urlDecode(result.substring(indexSplit + 1, result.length)) + } catch (e: Exception) { + e.printStackTrace() + } + + result = result.substring(0, indexSplit) + } + + //part decode + val indexS = result.indexOf(":") + if (indexS < 0) { + result = Utils.decode(result) + } + + val legacyPattern = "^(.+?):(\\d+?)$".toRegex() + val match = legacyPattern.matchEntire(result) + if (match == null) { + return R.string.toast_incorrect_protocol + } + vmess.address = match.groupValues[1] + if (vmess.address.firstOrNull() == '[' && vmess.address.lastOrNull() == ']') + vmess.address = vmess.address.substring(1, vmess.address.length - 1) + vmess.port = match.groupValues[2].toInt() + vmess.subid = subid + + addSocksServer(vmess, -1) + } else { + return R.string.toast_incorrect_protocol + } + } catch (e: Exception) { + e.printStackTrace() + return -1 + } + return 0 + } + + private fun ResolveVmess4Kitsunebi(server: String): AngConfig.VmessBean { + + val vmess = AngConfig.VmessBean() + + var result = server.replace(VMESS_PROTOCOL, "") + val indexSplit = result.indexOf("?") + if (indexSplit > 0) { + result = result.substring(0, indexSplit) + } + result = Utils.decode(result) + + val arr1 = result.split('@') + if (arr1.count() != 2) { + return vmess + } + val arr21 = arr1[0].split(':') + val arr22 = arr1[1].split(':') + if (arr21.count() != 2 || arr21.count() != 2) { + return vmess + } + + vmess.address = arr22[0] + vmess.port = Utils.parseInt(arr22[1]) + vmess.security = arr21[0] + vmess.id = arr21[1] + + vmess.security = "chacha20-poly1305" + vmess.network = "tcp" + vmess.headerType = "none" + vmess.remarks = "Alien" + vmess.alterId = 0 + + return vmess + } + + /** + * share config + */ + fun shareConfig(index: Int): String { + try { + if (index < 0 || index > angConfig.vmess.count() - 1) { + return "" + } + + val vmess = angConfig.vmess[index] + if (angConfig.vmess[index].configType == AppConfig.EConfigType.Vmess) { + + val vmessQRCode = VmessQRCode() + vmessQRCode.v = vmess.configVersion.toString() + vmessQRCode.ps = vmess.remarks + vmessQRCode.add = vmess.address + vmessQRCode.port = vmess.port.toString() + vmessQRCode.id = vmess.id + vmessQRCode.aid = vmess.alterId.toString() + vmessQRCode.net = vmess.network + vmessQRCode.type = vmess.headerType + vmessQRCode.host = vmess.requestHost + vmessQRCode.path = vmess.path + vmessQRCode.tls = vmess.streamSecurity + val json = Gson().toJson(vmessQRCode) + val conf = VMESS_PROTOCOL + Utils.encode(json) + + return conf + } else if (angConfig.vmess[index].configType == AppConfig.EConfigType.Shadowsocks) { + val remark = "#" + Utils.urlEncode(vmess.remarks) + val url = String.format("%s:%s@%s:%s", + vmess.security, + vmess.id, + vmess.address, + vmess.port) + return SS_PROTOCOL + Utils.encode(url) + remark + } else if (angConfig.vmess[index].configType == AppConfig.EConfigType.Socks) { + val remark = "#" + Utils.urlEncode(vmess.remarks) + val url = String.format("%s:%s", + vmess.address, + vmess.port) + return SOCKS_PROTOCOL + Utils.encode(url) + remark + } else { + return "" + } + } catch (e: Exception) { + e.printStackTrace() + return "" + } + } + + /** + * share2Clipboard + */ + fun share2Clipboard(index: Int): Int { + try { + val conf = shareConfig(index) + if (TextUtils.isEmpty(conf)) { + return -1 + } + + Utils.setClipboard(app.applicationContext, conf) + + } catch (e: Exception) { + e.printStackTrace() + return -1 + } + return 0 + } + + /** + * share2Clipboard + */ + fun shareAll2Clipboard(): Int { + try { + val sb = StringBuilder() + for (k in 0 until angConfig.vmess.count()) { + val url = shareConfig(k) + if (TextUtils.isEmpty(url)) { + continue + } + sb.append(url) + sb.appendln() + } + if (sb.count() > 0) { + Utils.setClipboard(app.applicationContext, sb.toString()) + } + } catch (e: Exception) { + e.printStackTrace() + return -1 + } + return 0 + } + + /** + * share2QRCode + */ + fun share2QRCode(index: Int): Bitmap? { + try { + val conf = shareConfig(index) + if (TextUtils.isEmpty(conf)) { + return null + } + val bitmap = Utils.createQRCode(conf) + return bitmap + + } catch (e: Exception) { + e.printStackTrace() + return null + } + } + + /** + * shareFullContent2Clipboard + */ + fun shareFullContent2Clipboard(index: Int): Int { + try { + if (AngConfigManager.genStoreV2rayConfig(index)) { + val configContent = app.defaultDPreference.getPrefString(AppConfig.PREF_CURR_CONFIG, "") + Utils.setClipboard(app.applicationContext, configContent) + } else { + return -1 + } + } catch (e: Exception) { + e.printStackTrace() + return -1 + } + return 0 + } + + /** + * import customize config + */ + fun importCustomizeConfig(server: String?): Int { + try { + if (server == null || TextUtils.isEmpty(server)) { + return R.string.toast_none_data + } + + val guid = System.currentTimeMillis().toString() + app.defaultDPreference.setPrefString(ANG_CONFIG + guid, server) + + //add + val vmess = AngConfig.VmessBean() + vmess.configVersion = 2 + vmess.configType = AppConfig.EConfigType.Custom + vmess.guid = guid + vmess.remarks = vmess.guid + + vmess.security = "" + vmess.network = "" + vmess.headerType = "" + vmess.address = "" + vmess.port = 0 + vmess.id = "" + vmess.alterId = 0 + vmess.network = "" + vmess.headerType = "" + vmess.requestHost = "" + vmess.streamSecurity = "" + + angConfig.vmess.add(vmess) + if (angConfig.vmess.count() == 1) { + angConfig.index = 0 + } + storeConfigFile() + } catch (e: Exception) { + e.printStackTrace() + return -1 + } + return 0 + } + + /** + * getIndexViaGuid + */ + fun getIndexViaGuid(guid: String): Int { + try { + if (TextUtils.isEmpty(guid)) { + return -1 + } + for (i in angConfig.vmess.indices) { + if (angConfig.vmess[i].guid == guid) { + return i + } + } + return -1 + } catch (e: Exception) { + e.printStackTrace() + return -1 + } + } + + /** + * upgrade + */ + fun upgradeServerVersion(vmess: AngConfig.VmessBean): Int { + try { + if (vmess.configVersion == 2) { + return 0 + } + + when (vmess.network) { + "kcp" -> { + } + "ws" -> { + var path = "" + var host = "" + val lstParameter = vmess.requestHost.split(";") + if (lstParameter.size > 0) { + path = lstParameter.get(0).trim() + } + if (lstParameter.size > 1) { + path = lstParameter.get(0).trim() + host = lstParameter.get(1).trim() + } + vmess.path = path + vmess.requestHost = host + } + "h2" -> { + var path = "" + var host = "" + val lstParameter = vmess.requestHost.split(";") + if (lstParameter.size > 0) { + path = lstParameter.get(0).trim() + } + if (lstParameter.size > 1) { + path = lstParameter.get(0).trim() + host = lstParameter.get(1).trim() + } + vmess.path = path + vmess.requestHost = host + } + else -> { + } + } + vmess.configVersion = 2 + return 0 + } catch (e: Exception) { + e.printStackTrace() + return -1 + } + } + + + fun addCustomServer(vmess: AngConfig.VmessBean, index: Int): Int { + try { + vmess.configVersion = 2 + vmess.configType = AppConfig.EConfigType.Custom + + if (index >= 0) { + //edit + angConfig.vmess[index] = vmess + } else { + //add + vmess.guid = System.currentTimeMillis().toString() + angConfig.vmess.add(vmess) + if (angConfig.vmess.count() == 1) { + angConfig.index = 0 + } + } + + storeConfigFile() + } catch (e: Exception) { + e.printStackTrace() + return -1 + } + return 0 + } + + fun addShadowsocksServer(vmess: AngConfig.VmessBean, index: Int): Int { + try { + vmess.configVersion = 2 + vmess.configType = AppConfig.EConfigType.Shadowsocks + + if (index >= 0) { + //edit + angConfig.vmess[index] = vmess + } else { + //add + vmess.guid = System.currentTimeMillis().toString() + angConfig.vmess.add(vmess) + if (angConfig.vmess.count() == 1) { + angConfig.index = 0 + } + } + + storeConfigFile() + } catch (e: Exception) { + e.printStackTrace() + return -1 + } + return 0 + } + + fun addSocksServer(vmess: AngConfig.VmessBean, index: Int): Int { + try { + vmess.configVersion = 2 + vmess.configType = AppConfig.EConfigType.Socks + + if (index >= 0) { + //edit + angConfig.vmess[index] = vmess + } else { + //add + vmess.guid = System.currentTimeMillis().toString() + angConfig.vmess.add(vmess) + if (angConfig.vmess.count() == 1) { + angConfig.index = 0 + } + } + + storeConfigFile() + } catch (e: Exception) { + e.printStackTrace() + return -1 + } + return 0 + } + + fun importBatchConfig(servers: String?, subid: String): Int { + try { + if (servers == null) { + return 0 + } + removeServerViaSubid(subid) + +// var servers = server +// if (server.indexOf("vmess") >= 0 && server.indexOf("vmess") == server.lastIndexOf("vmess")) { +// servers = server.replace("\n", "") +// } + + var count = 0 + servers.lines() + .forEach { + val resId = importConfig(it, subid) + if (resId == 0) { + count++ + } + } + return count + } catch (e: Exception) { + e.printStackTrace() + } + return 0 + } + + fun saveSubItem(subItem: ArrayList): Int { + try { + if (subItem.count() <= 0) { + return -1 + } + for (k in 0 until subItem.count()) { + if (TextUtils.isEmpty(subItem[k].id)) { + subItem[k].id = Utils.getUuid() + } + } + angConfig.subItem = subItem + + storeConfigFile() + } catch (e: Exception) { + e.printStackTrace() + return -1 + } + return 0 + } + + fun removeServerViaSubid(subid: String): Int { + if (TextUtils.isEmpty(subid) || configs.vmess.count() <= 0) { + return -1 + } + + for (k in configs.vmess.count() - 1 downTo 0) { + if (configs.vmess[k].subid.equals(subid)) { + angConfig.vmess.removeAt(k) + } + } + + storeConfigFile() + return 0 + } + + fun addSubItem(subItem: AngConfig.SubItemBean, index: Int): Int { + try { + if (index >= 0) { + //edit + angConfig.subItem[index] = subItem + } else { + //add + angConfig.subItem.add(subItem) + } + + saveSubItem(angConfig.subItem) + } catch (e: Exception) { + e.printStackTrace() + return -1 + } + return 0 + } + + /** + * + */ + fun removeSubItem(index: Int): Int { + try { + if (index < 0 || index > angConfig.subItem.count() - 1) { + return -1 + } + + //删除 + angConfig.subItem.removeAt(index) + + storeConfigFile() + } catch (e: Exception) { + e.printStackTrace() + return -1 + } + return 0 + } + +} \ No newline at end of file diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/AppManagerUtil.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/AppManagerUtil.kt new file mode 100644 index 000000000..a806ee5df --- /dev/null +++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/AppManagerUtil.kt @@ -0,0 +1,43 @@ +package com.v2ray.ang.util + +import android.Manifest +import android.content.Context +import android.content.pm.ApplicationInfo +import android.content.pm.PackageInfo +import android.content.pm.PackageManager +import com.v2ray.ang.dto.AppInfo +import rx.Observable +import java.util.* + +object AppManagerUtil { + fun loadNetworkAppList(ctx: Context): ArrayList { + val packageManager = ctx.packageManager + val packages = packageManager.getInstalledPackages(PackageManager.GET_PERMISSIONS) + val apps = ArrayList() + + for (pkg in packages) { + if (!pkg.hasInternetPermission) continue + + val applicationInfo = pkg.applicationInfo + + val appName = applicationInfo.loadLabel(packageManager).toString() + val appIcon = applicationInfo.loadIcon(packageManager) + val isSystemApp = (applicationInfo.flags and ApplicationInfo.FLAG_SYSTEM) > 0 + + val appInfo = AppInfo(appName, pkg.packageName, appIcon, isSystemApp, 0) + apps.add(appInfo) + } + + return apps + } + + fun rxLoadNetworkAppList(ctx: Context): Observable> = Observable.create { + it.onNext(loadNetworkAppList(ctx)) + } + + val PackageInfo.hasInternetPermission: Boolean + get() { + val permissions = requestedPermissions + return permissions?.any { it == Manifest.permission.INTERNET } ?: false + } +} \ No newline at end of file diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/MessageUtil.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/MessageUtil.kt new file mode 100644 index 000000000..486023c8e --- /dev/null +++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/MessageUtil.kt @@ -0,0 +1,30 @@ +package com.v2ray.ang.util + +import android.content.Context +import android.content.Intent +import com.v2ray.ang.AppConfig + + +object MessageUtil { + + fun sendMsg2Service(ctx: Context, what: Int, content: String) { + sendMsg(ctx, AppConfig.BROADCAST_ACTION_SERVICE, what, content) + } + + fun sendMsg2UI(ctx: Context, what: Int, content: String) { + sendMsg(ctx, AppConfig.BROADCAST_ACTION_ACTIVITY, what, content) + } + + private fun sendMsg(ctx: Context, action: String, what: Int, content: String) { + try { + val intent = Intent() + intent.action = action + intent.`package` = AppConfig.ANG_PACKAGE + intent.putExtra("key", what) + intent.putExtra("content", content) + ctx.sendBroadcast(intent) + } catch (e: Exception) { + e.printStackTrace() + } + } +} \ No newline at end of file diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/Utils.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/Utils.kt new file mode 100644 index 000000000..343137901 --- /dev/null +++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/Utils.kt @@ -0,0 +1,509 @@ +package com.v2ray.ang.util + +import android.content.ClipboardManager +import android.content.Context +import android.text.Editable +import android.util.Base64 +import com.google.zxing.WriterException +import android.graphics.Bitmap +import com.google.zxing.BarcodeFormat +import com.google.zxing.qrcode.QRCodeWriter +import com.google.zxing.EncodeHintType +import java.util.* +import kotlin.collections.HashMap +import android.app.ActivityManager +import android.content.ClipData +import android.content.Intent +import android.content.res.AssetManager +import android.net.Uri +import android.os.SystemClock +import android.text.TextUtils +import android.text.method.ScrollingMovementMethod +import android.util.Log +import android.util.Patterns +import android.view.View +import android.webkit.URLUtil +import com.v2ray.ang.AngApplication +import com.v2ray.ang.AppConfig +import com.v2ray.ang.R +import com.v2ray.ang.extension.responseLength +import com.v2ray.ang.service.V2RayVpnService +import com.v2ray.ang.ui.SettingsActivity +import kotlinx.android.synthetic.main.activity_logcat.* +import me.dozen.dpreference.DPreference +import org.jetbrains.anko.toast +import org.jetbrains.anko.uiThread +import java.io.BufferedReader +import java.io.File +import java.io.IOException +import java.io.InputStreamReader +import java.net.* +import java.util.regex.Matcher +import java.util.regex.Pattern +import java.math.BigInteger +import java.util.concurrent.TimeUnit +import libv2ray.Libv2ray + + +object Utils { + + /** + * convert string to editalbe for kotlin + * + * @param text + * @return + */ + fun getEditable(text: String): Editable { + return Editable.Factory.getInstance().newEditable(text) + } + + /** + * find value in array position + */ + fun arrayFind(array: Array, value: String): Int { + for (i in array.indices) { + if (array[i] == value) { + return i + } + } + return -1 + } + + /** + * parseInt + */ + fun parseInt(str: String): Int { + try { + return Integer.parseInt(str) + } catch (e: Exception) { + e.printStackTrace() + return 0 + } + } + + /** + * get text from clipboard + */ + fun getClipboard(context: Context): String { + try { + val cmb = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + return cmb.primaryClip?.getItemAt(0)?.text.toString() + } catch (e: Exception) { + e.printStackTrace() + return "" + } + } + + /** + * set text to clipboard + */ + fun setClipboard(context: Context, content: String) { + try { + val cmb = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + val clipData = ClipData.newPlainText(null, content) + cmb.primaryClip = clipData + } catch (e: Exception) { + e.printStackTrace() + } + } + + /** + * base64 decode + */ + fun decode(text: String): String { + try { + return Base64.decode(text, Base64.NO_WRAP).toString(charset("UTF-8")) + } catch (e: Exception) { + e.printStackTrace() + return "" + } + } + + /** + * base64 encode + */ + fun encode(text: String): String { + try { + return Base64.encodeToString(text.toByteArray(charset("UTF-8")), Base64.NO_WRAP) + } catch (e: Exception) { + e.printStackTrace() + return "" + } + } + + /** + * get remote dns servers from preference + */ + fun getRemoteDnsServers(defaultDPreference: DPreference): ArrayList { + val remoteDns = defaultDPreference.getPrefString(SettingsActivity.PREF_REMOTE_DNS, AppConfig.DNS_AGENT) + val ret = ArrayList() + if (!TextUtils.isEmpty(remoteDns)) { + remoteDns + .split(",") + .forEach { + if (Utils.isPureIpAddress(it)) { + ret.add(it) + } + } + } + if (ret.size == 0) { + ret.add(AppConfig.DNS_AGENT) + } + return ret + } + + /** + * get remote dns servers from preference + */ + fun getDomesticDnsServers(defaultDPreference: DPreference): ArrayList { + val domesticDns = defaultDPreference.getPrefString(SettingsActivity.PREF_DOMESTIC_DNS, AppConfig.DNS_DIRECT) + val ret = ArrayList() + if (!TextUtils.isEmpty(domesticDns)) { + domesticDns + .split(",") + .forEach { + if (Utils.isPureIpAddress(it)) { + ret.add(it) + } + } + } + if (ret.size == 0) { + ret.add(AppConfig.DNS_DIRECT) + } + return ret + } + + /** + * create qrcode using zxing + */ + fun createQRCode(text: String, size: Int = 800): Bitmap? { + try { + val hints = HashMap() + hints.put(EncodeHintType.CHARACTER_SET, "utf-8") + val bitMatrix = QRCodeWriter().encode(text, + BarcodeFormat.QR_CODE, size, size, hints) + val pixels = IntArray(size * size) + for (y in 0..size - 1) { + for (x in 0..size - 1) { + if (bitMatrix.get(x, y)) { + pixels[y * size + x] = 0xff000000.toInt() + } else { + pixels[y * size + x] = 0xffffffff.toInt() + } + + } + } + val bitmap = Bitmap.createBitmap(size, size, + Bitmap.Config.ARGB_8888) + bitmap.setPixels(pixels, 0, size, 0, 0, size, size) + return bitmap + } catch (e: WriterException) { + e.printStackTrace() + return null + } + } + + /** + * is ip address + */ + fun isIpAddress(value: String): Boolean { + try { + var addr = value + if (addr.isEmpty() || addr.isBlank()) { + return false + } + //CIDR + if (addr.indexOf("/") > 0) { + val arr = addr.split("/") + if (arr.count() == 2 && Integer.parseInt(arr[1]) > 0) { + addr = arr[0] + } + } + + // "::ffff:192.168.173.22" + // "[::ffff:192.168.173.22]:80" + if (addr.startsWith("::ffff:") && '.' in addr) { + addr = addr.drop(7) + } else if (addr.startsWith("[::ffff:") && '.' in addr) { + addr = addr.drop(8).replace("]", "") + } + + // addr = addr.toLowerCase() + var octets = addr.split('.').toTypedArray() + if (octets.size == 4) { + if(octets[3].indexOf(":") > 0) { + addr = addr.substring(0, addr.indexOf(":")) + } + return isIpv4Address(addr) + } + + // Ipv6addr [2001:abc::123]:8080 + return isIpv6Address(addr) + } catch (e: Exception) { + e.printStackTrace() + return false + } + } + + fun isPureIpAddress(value: String): Boolean { + return (isIpv4Address(value) || isIpv6Address(value)) + } + + fun isIpv4Address(value: String): Boolean { + val regV4 = Regex("^([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\\.([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\\.([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\\.([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])$") + return regV4.matches(value) + } + + fun isIpv6Address(value: String): Boolean { + var addr = value + if (addr.indexOf("[") == 0 && addr.lastIndexOf("]") > 0) { + addr = addr.drop(1) + addr = addr.dropLast(addr.count() - addr.lastIndexOf("]")) + } + val regV6 = Regex("^((?:[0-9A-Fa-f]{1,4}))?((?::[0-9A-Fa-f]{1,4}))*::((?:[0-9A-Fa-f]{1,4}))?((?::[0-9A-Fa-f]{1,4}))*|((?:[0-9A-Fa-f]{1,4}))((?::[0-9A-Fa-f]{1,4})){7}$") + return regV6.matches(addr) + } + + /** + * is valid url + */ + fun isValidUrl(value: String?): Boolean { + try { + if (Patterns.WEB_URL.matcher(value).matches() || URLUtil.isValidUrl(value)) { + return true + } + } catch (e: WriterException) { + e.printStackTrace() + return false + } + return false + } + + + /** + * 判断服务是否后台运行 + + * @param context + * * Context + * * + * @param className + * * 判断的服务名字 + * * + * @return true 在运行 false 不在运行 + */ + fun isServiceRun(context: Context, className: String): Boolean { + var isRun = false + val activityManager = context + .getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager + val serviceList = activityManager + .getRunningServices(999) + val size = serviceList.size + for (i in 0..size - 1) { + if (serviceList[i].service.className == className) { + isRun = true + break + } + } + return isRun + } + + /** + * startVService + */ + fun startVService(context: Context): Boolean { + context.toast(R.string.toast_services_start) + if (AngConfigManager.genStoreV2rayConfig(-1)) { + val configContent = AngConfigManager.currGeneratedV2rayConfig() + val configType = AngConfigManager.currConfigType() + if (configType == AppConfig.EConfigType.Custom) { + try { + Libv2ray.testConfig(configContent) + } catch (e: Exception) { + context.toast(e.toString()) + return false + } + } + V2RayVpnService.startV2Ray(context) + return true + } else { + return false + } + } + + /** + * startVService + */ + fun startVService(context: Context, guid: String): Boolean { + val index = AngConfigManager.getIndexViaGuid(guid) + return startVService(context, index) + } + + /** + * startVService + */ + fun startVService(context: Context, index: Int): Boolean { + AngConfigManager.setActiveServer(index) + return startVService(context) + } + + /** + * stopVService + */ + fun stopVService(context: Context) { + context.toast(R.string.toast_services_stop) + MessageUtil.sendMsg2Service(context, AppConfig.MSG_STATE_STOP, "") + } + + fun openUri(context: Context, uriString: String) { + val uri = Uri.parse(uriString) + context.startActivity(Intent(Intent.ACTION_VIEW, uri)) + } + + /** + * uuid + */ + fun getUuid(): String { + try { + return UUID.randomUUID().toString().replace("-", "") + } catch (e: Exception) { + e.printStackTrace() + return "" + } + } + + fun urlDecode(url: String): String { + try { + return URLDecoder.decode(url, "UTF-8") + } catch (e: Exception) { + e.printStackTrace() + return url + } + } + + fun urlEncode(url: String): String { + try { + return URLEncoder.encode(url, "UTF-8") + } catch (e: Exception) { + e.printStackTrace() + return url + } + } + + fun testConnection(context: Context, port: Int): String { + var result: String + var conn: HttpURLConnection? = null + + try { + val url = URL("https", + "www.google.com", + "/generate_204") + + conn = url.openConnection( + Proxy(Proxy.Type.HTTP, + InetSocketAddress("127.0.0.1", port + 1))) as HttpURLConnection + conn.connectTimeout = 30000 + conn.readTimeout = 30000 + conn.setRequestProperty("Connection", "close") + conn.instanceFollowRedirects = false + conn.useCaches = false + + val start = SystemClock.elapsedRealtime() + val code = conn.responseCode + val elapsed = SystemClock.elapsedRealtime() - start + + if (code == 204 || code == 200 && conn.responseLength == 0L) { + result = context.getString(R.string.connection_test_available, elapsed) + } else { + throw IOException(context.getString(R.string.connection_test_error_status_code, code)) + } + } catch (e: IOException) { + // network exception + Log.d(AppConfig.ANG_PACKAGE,"testConnection IOException: "+Log.getStackTraceString(e)) + result = context.getString(R.string.connection_test_error, e.message) + } catch (e: Exception) { + // library exception, eg sumsung + Log.d(AppConfig.ANG_PACKAGE,"testConnection Exception: "+Log.getStackTraceString(e)) + result = context.getString(R.string.connection_test_error, e.message) + } finally { + conn?.disconnect() + } + + return result + } + + /** + * package path + */ + fun packagePath(context: Context): String { + var path = context.filesDir.toString() + path = path.replace("files", "") + //path += "tun2socks" + + return path + } + + + /** + * readTextFromAssets + */ + fun readTextFromAssets(app: AngApplication, fileName: String): String { + val content = app.assets.open(fileName).bufferedReader().use { + it.readText() + } + return content + } + + /** + * ping + */ + fun ping(url: String): String { + try { + val command = "/system/bin/ping -c 3 $url" + val process = Runtime.getRuntime().exec(command) + val allText = process.inputStream.bufferedReader().use { it.readText() } + if (!TextUtils.isEmpty(allText)) { + val tempInfo = allText.substring(allText.indexOf("min/avg/max/mdev") + 19) + val temps = tempInfo.split("/".toRegex()).dropLastWhile({ it.isEmpty() }).toTypedArray() + if (temps.count() > 0 && temps[0].length < 10) { + return temps[0].toFloat().toInt().toString() + "ms" + } + } + } catch (e: Exception) { + e.printStackTrace() + } + return "-1ms" + } + + /** + * tcping + */ + fun tcping(url: String, port: Int): String { + var time = -1L + for (k in 0 until 2) { + val one = socketConnectTime(url, port) + if (one != -1L ) + if(time == -1L || one < time) { + time = one + } + } + return time.toString() + "ms" + } + + fun socketConnectTime(url: String, port: Int): Long { + try { + val start = System.currentTimeMillis() + val socket = Socket(url, port) + val time = System.currentTimeMillis() - start + socket.close() + return time + } catch (e: UnknownHostException) { + e.printStackTrace() + } catch (e: IOException) { + e.printStackTrace() + } catch (e: Exception) { + e.printStackTrace() + } + return -1 + } +} + diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/V2rayConfigUtil.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/V2rayConfigUtil.kt new file mode 100644 index 000000000..ca3deb783 --- /dev/null +++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/V2rayConfigUtil.kt @@ -0,0 +1,677 @@ +package com.v2ray.ang.util + +import android.text.TextUtils +import android.util.Log +import com.google.gson.Gson +import com.google.gson.GsonBuilder +import com.google.gson.JsonArray +import com.v2ray.ang.AngApplication +import com.v2ray.ang.AppConfig +import com.v2ray.ang.dto.AngConfig.VmessBean +import com.v2ray.ang.dto.V2rayConfig +import com.v2ray.ang.extension.defaultDPreference +import com.v2ray.ang.ui.SettingsActivity +import org.json.JSONException +import org.json.JSONObject +import org.json.JSONArray +import com.google.gson.JsonObject + +object V2rayConfigUtil { + private val requestObj: JsonObject by lazy { + Gson().fromJson("""{"version":"1.1","method":"GET","path":["/"],"headers":{"User-Agent":["Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.75 Safari/537.36","Mozilla/5.0 (iPhone; CPU iPhone OS 10_0_2 like Mac OS X) AppleWebKit/601.1 (KHTML, like Gecko) CriOS/53.0.2785.109 Mobile/14A456 Safari/601.1.46"],"Accept-Encoding":["gzip, deflate"],"Connection":["keep-alive"],"Pragma":"no-cache"}}""", JsonObject::class.java) + } + +// private val responseObj: JSONObject by lazy { +// JSONObject("""{"version":"1.1","status":"200","reason":"OK","headers":{"Content-Type":["application/octet-stream","video/mpeg"],"Transfer-Encoding":["chunked"],"Connection":["keep-alive"],"Pragma":"no-cache"}}""") +// } + + data class Result(var status: Boolean, var content: String) + + /** + * 生成v2ray的客户端配置文件 + */ + fun getV2rayConfig(app: AngApplication, vmess: VmessBean): Result { + var result = Result(false, "") + try { + //检查设置 +// if (config.index < 0 +// || config.vmess.count() <= 0 +// || config.index > config.vmess.count() - 1 +// ) { +// return result +// } + + if (vmess.configType == AppConfig.EConfigType.Vmess) { + result = getV2rayConfigType1(app, vmess) + } else if (vmess.configType == AppConfig.EConfigType.Custom) { + result = getV2rayConfigType2(app, vmess) + } else if (vmess.configType == AppConfig.EConfigType.Shadowsocks) { + result = getV2rayConfigType1(app, vmess) + } else if (vmess.configType == AppConfig.EConfigType.Socks) { + result = getV2rayConfigType1(app, vmess) + } + + val domainName = parseDomainName(result.content) + if (!TextUtils.isEmpty(domainName)) { + app.defaultDPreference.setPrefString(AppConfig.PREF_CURR_CONFIG_DOMAIN, domainName) + } + + Log.d("V2rayConfigUtilGoLog", result.content) + return result + } catch (e: Exception) { + e.printStackTrace() + return result + } + } + + /** + * 生成v2ray的客户端配置文件 + */ + private fun getV2rayConfigType1(app: AngApplication, vmess: VmessBean): Result { + val result = Result(false, "") + try { + //取得默认配置 + val assets = Utils.readTextFromAssets(app, "v2ray_config.json") + if (TextUtils.isEmpty(assets)) { + return result + } + + //转成Json + val v2rayConfig = Gson().fromJson(assets, V2rayConfig::class.java) ?: return result +// if (v2rayConfig == null) { +// return result +// } + + inbounds(vmess, v2rayConfig, app) + + outbounds(vmess, v2rayConfig, app) + + routing(vmess, v2rayConfig, app) + + if (app.defaultDPreference.getPrefBoolean(SettingsActivity.PREF_LOCAL_DNS_ENABLED, false)) { + customLocalDns(vmess, v2rayConfig, app) + } else { + customRemoteDns(vmess, v2rayConfig, app) + } + + val finalConfig = GsonBuilder().setPrettyPrinting().create().toJson(v2rayConfig) + + result.status = true + result.content = finalConfig + return result + + } catch (e: Exception) { + e.printStackTrace() + return result + } + } + + /** + * 生成v2ray的客户端配置文件 + */ + private fun getV2rayConfigType2(app: AngApplication, vmess: VmessBean): Result { + val result = Result(false, "") + try { + val guid = vmess.guid + val jsonConfig = app.defaultDPreference.getPrefString(AppConfig.ANG_CONFIG + guid, "") + result.status = true + result.content = jsonConfig + return result + + } catch (e: Exception) { + e.printStackTrace() + return result + } + } + + /** + * + */ + private fun inbounds(vmess: VmessBean, v2rayConfig: V2rayConfig, app: AngApplication): Boolean { + try { + v2rayConfig.inbounds[0].port = 10808 +// val socksPort = Utils.parseInt(app.defaultDPreference.getPrefString(SettingsActivity.PREF_SOCKS_PORT, "10808")) +// val lanconnPort = Utils.parseInt(app.defaultDPreference.getPrefString(SettingsActivity.PREF_HTTP_PORT, "")) + +// if (socksPort > 0) { +// v2rayConfig.inbounds[0].port = socksPort +// } +// if (lanconnPort > 0) { +// val httpCopy = v2rayConfig.inbounds[0].copy() +// httpCopy.port = lanconnPort +// httpCopy.protocol = "http" +// v2rayConfig.inbounds.add(httpCopy) +// } + v2rayConfig.inbounds[0].sniffing?.enabled = app.defaultDPreference.getPrefBoolean(SettingsActivity.PREF_SNIFFING_ENABLED, true) + + } catch (e: Exception) { + e.printStackTrace() + return false + } + return true + } + + /** + * vmess协议服务器配置 + */ + private fun outbounds(vmess: VmessBean, v2rayConfig: V2rayConfig, app: AngApplication): Boolean { + try { + val outbound = v2rayConfig.outbounds[0] + + when (vmess.configType) { + AppConfig.EConfigType.Vmess -> { + outbound.settings?.servers = null + + val vnext = v2rayConfig.outbounds[0].settings?.vnext?.get(0) + vnext?.address = vmess.address + vnext?.port = vmess.port + val user = vnext?.users?.get(0) + user?.id = vmess.id + user?.alterId = vmess.alterId + user?.security = vmess.security + user?.level = 8 + + //Mux + val muxEnabled = false//app.defaultDPreference.getPrefBoolean(SettingsActivity.PREF_MUX_ENABLED, false) + outbound.mux?.enabled = muxEnabled + + //远程服务器底层传输配置 + outbound.streamSettings = boundStreamSettings(vmess) + + outbound.protocol = "vmess" + } + AppConfig.EConfigType.Shadowsocks -> { + outbound.settings?.vnext = null + + val server = outbound.settings?.servers?.get(0) + server?.address = vmess.address + server?.method = vmess.security + server?.ota = false + server?.password = vmess.id + server?.port = vmess.port + server?.level = 8 + + //Mux + outbound.mux?.enabled = false + + outbound.protocol = "shadowsocks" + } + AppConfig.EConfigType.Socks -> { + outbound.settings?.vnext = null + + val server = outbound.settings?.servers?.get(0) + server?.address = vmess.address + server?.port = vmess.port + + //Mux + outbound.mux?.enabled = false + + outbound.protocol = "socks" + } + else -> { + } + } + + var serverDomain: String + if(Utils.isIpv6Address(vmess.address)) { + serverDomain = String.format("[%s]:%s", vmess.address, vmess.port) + } else { + serverDomain = String.format("%s:%s", vmess.address, vmess.port) + } + app.defaultDPreference.setPrefString(AppConfig.PREF_CURR_CONFIG_DOMAIN, serverDomain) + + } catch (e: Exception) { + e.printStackTrace() + return false + } + return true + } + + /** + * 远程服务器底层传输配置 + */ + private fun boundStreamSettings(vmess: VmessBean): V2rayConfig.OutboundBean.StreamSettingsBean { + val streamSettings = V2rayConfig.OutboundBean.StreamSettingsBean("", "", null, null, null, null, null, null) + try { + //远程服务器底层传输配置 + streamSettings.network = vmess.network + streamSettings.security = vmess.streamSecurity + + //streamSettings + when (streamSettings.network) { + "kcp" -> { + val kcpsettings = V2rayConfig.OutboundBean.StreamSettingsBean.KcpsettingsBean() + kcpsettings.mtu = 1350 + kcpsettings.tti = 50 + kcpsettings.uplinkCapacity = 12 + kcpsettings.downlinkCapacity = 100 + kcpsettings.congestion = false + kcpsettings.readBufferSize = 1 + kcpsettings.writeBufferSize = 1 + kcpsettings.header = V2rayConfig.OutboundBean.StreamSettingsBean.KcpsettingsBean.HeaderBean() + kcpsettings.header.type = vmess.headerType + streamSettings.kcpsettings = kcpsettings + } + "ws" -> { + val wssettings = V2rayConfig.OutboundBean.StreamSettingsBean.WssettingsBean() + wssettings.connectionReuse = true + val host = vmess.requestHost.trim() + val path = vmess.path.trim() + + if (!TextUtils.isEmpty(host)) { + wssettings.headers = V2rayConfig.OutboundBean.StreamSettingsBean.WssettingsBean.HeadersBean() + wssettings.headers.Host = host + } + if (!TextUtils.isEmpty(path)) { + wssettings.path = path + } + streamSettings.wssettings = wssettings + + val tlssettings = V2rayConfig.OutboundBean.StreamSettingsBean.TlssettingsBean() + tlssettings.allowInsecure = true + if (!TextUtils.isEmpty(host)) { + tlssettings.serverName = host + } + streamSettings.tlssettings = tlssettings + } + "h2" -> { + val httpsettings = V2rayConfig.OutboundBean.StreamSettingsBean.HttpsettingsBean() + val host = vmess.requestHost.trim() + val path = vmess.path.trim() + + if (!TextUtils.isEmpty(host)) { + httpsettings.host = host.split(",").map { it.trim() } + } + httpsettings.path = path + streamSettings.httpsettings = httpsettings + + val tlssettings = V2rayConfig.OutboundBean.StreamSettingsBean.TlssettingsBean() + tlssettings.allowInsecure = true + streamSettings.tlssettings = tlssettings + } + "quic" -> { + val quicsettings = V2rayConfig.OutboundBean.StreamSettingsBean.QuicsettingBean() + val host = vmess.requestHost.trim() + val path = vmess.path.trim() + + quicsettings.security = host + quicsettings.key = path + + quicsettings.header = V2rayConfig.OutboundBean.StreamSettingsBean.QuicsettingBean.HeaderBean() + quicsettings.header.type = vmess.headerType + + streamSettings.quicsettings = quicsettings + } + else -> { + //tcp带http伪装 + if (vmess.headerType == "http") { + val tcpSettings = V2rayConfig.OutboundBean.StreamSettingsBean.TcpsettingsBean() + tcpSettings.connectionReuse = true + tcpSettings.header = V2rayConfig.OutboundBean.StreamSettingsBean.TcpsettingsBean.HeaderBean() + tcpSettings.header.type = vmess.headerType + +// if (requestObj.has("headers") +// || requestObj.optJSONObject("headers").has("Pragma")) { +// val arrHost = ArrayList() +// vmess.requestHost +// .split(",") +// .forEach { +// arrHost.add(it) +// } +// requestObj.optJSONObject("headers") +// .put("Host", arrHost) +// +// } + if (!TextUtils.isEmpty(vmess.requestHost)) { + val arrHost = ArrayList() + vmess.requestHost + .split(",") + .forEach { + arrHost.add("\"$it\"") + } + requestObj.getAsJsonObject("headers") + .add("Host", Gson().fromJson(arrHost.toString(), JsonArray::class.java)) + } + if (!TextUtils.isEmpty(vmess.path)) { + val arrPath = ArrayList() + vmess.path + .split(",") + .forEach { + arrPath.add("\"$it\"") + } + requestObj.add("path", Gson().fromJson(arrPath.toString(), JsonArray::class.java)) + } + tcpSettings.header.request = requestObj + //tcpSettings.header.response = responseObj + streamSettings.tcpSettings = tcpSettings + } + } + } + } catch (e: Exception) { + e.printStackTrace() + return streamSettings + } + return streamSettings + } + + /** + * routing + */ + private fun routing(vmess: VmessBean, v2rayConfig: V2rayConfig, app: AngApplication): Boolean { + try { + routingUserRule(app.defaultDPreference.getPrefString(AppConfig.PREF_V2RAY_ROUTING_AGENT, ""), AppConfig.TAG_AGENT, v2rayConfig) + routingUserRule(app.defaultDPreference.getPrefString(AppConfig.PREF_V2RAY_ROUTING_DIRECT, ""), AppConfig.TAG_DIRECT, v2rayConfig) + routingUserRule(app.defaultDPreference.getPrefString(AppConfig.PREF_V2RAY_ROUTING_BLOCKED, ""), AppConfig.TAG_BLOCKED, v2rayConfig) + + v2rayConfig.routing.domainStrategy = app.defaultDPreference.getPrefString(SettingsActivity.PREF_ROUTING_DOMAIN_STRATEGY, "IPIfNonMatch") + val routingMode = app.defaultDPreference.getPrefString(SettingsActivity.PREF_ROUTING_MODE, "0") + + // Hardcode googleapis.cn + val googleapisRoute = V2rayConfig.RoutingBean.RulesBean( + type = "field", + outboundTag = AppConfig.TAG_AGENT, + domain = arrayListOf("domain:googleapis.cn") + ) + + when (routingMode) { + "0" -> { + } + "1" -> { + routingGeo("ip", "private", AppConfig.TAG_DIRECT, v2rayConfig) + } + "2" -> { + routingGeo("", "cn", AppConfig.TAG_DIRECT, v2rayConfig) + v2rayConfig.routing.rules.add(0, googleapisRoute) + } + "3" -> { + routingGeo("ip", "private", AppConfig.TAG_DIRECT, v2rayConfig) + routingGeo("", "cn", AppConfig.TAG_DIRECT, v2rayConfig) + v2rayConfig.routing.rules.add(0, googleapisRoute) + } + } + } catch (e: Exception) { + e.printStackTrace() + return false + } + return true + } + + private fun routingGeo(ipOrDomain: String, code: String, tag: String, v2rayConfig: V2rayConfig) { + try { + if (!TextUtils.isEmpty(code)) { + //IP + if (ipOrDomain == "ip" || ipOrDomain == "") { + val rulesIP = V2rayConfig.RoutingBean.RulesBean() + rulesIP.type = "field" + rulesIP.outboundTag = tag + rulesIP.ip = ArrayList() + rulesIP.ip?.add("geoip:$code") + v2rayConfig.routing.rules.add(rulesIP) + } + + if (ipOrDomain == "domain" || ipOrDomain == "") { + //Domain + val rulesDomain = V2rayConfig.RoutingBean.RulesBean() + rulesDomain.type = "field" + rulesDomain.outboundTag = tag + rulesDomain.domain = ArrayList() + rulesDomain.domain?.add("geosite:$code") + v2rayConfig.routing.rules.add(rulesDomain) + } + } + } catch (e: Exception) { + e.printStackTrace() + } + } + + private fun routingUserRule(userRule: String, tag: String, v2rayConfig: V2rayConfig) { + try { + if (!TextUtils.isEmpty(userRule)) { + //Domain + val rulesDomain = V2rayConfig.RoutingBean.RulesBean() + rulesDomain.type = "field" + rulesDomain.outboundTag = tag + rulesDomain.domain = ArrayList() + + //IP + val rulesIP = V2rayConfig.RoutingBean.RulesBean() + rulesIP.type = "field" + rulesIP.outboundTag = tag + rulesIP.ip = ArrayList() + + userRule.trim().replace("\n", "") + .split(",") + .forEach { + if (Utils.isIpAddress(it) || it.startsWith("geoip:")) { + rulesIP.ip?.add(it) + } else if (it.isNotBlank() || it.isNotEmpty()) +// if (Utils.isValidUrl(it) +// || it.startsWith("geosite:") +// || it.startsWith("regexp:") +// || it.startsWith("domain:") +// || it.startsWith("full:")) + { + rulesDomain.domain?.add(it) + } + } + if (rulesDomain.domain?.size!! > 0) { + v2rayConfig.routing.rules.add(rulesDomain) + } + if (rulesIP.ip?.size!! > 0) { + v2rayConfig.routing.rules.add(rulesIP) + } + } + } catch (e: Exception) { + e.printStackTrace() + } + } + + private fun userRule2Domian(userRule: String): ArrayList { + val domain = ArrayList() + userRule.trim().replace("\n", "").split(",").forEach { + if ((it.startsWith("geosite:") || it.startsWith("domain:")) && + it.isNotBlank() && it.isNotEmpty()) { + domain.add(it) + } + } + return domain + } + + /** + * Custom Dns + */ + private fun customLocalDns(vmess: VmessBean, v2rayConfig: V2rayConfig, app: AngApplication): Boolean { + try { + val hosts = mutableMapOf() + val servers = ArrayList() + val remoteDns = Utils.getRemoteDnsServers(app.defaultDPreference) + remoteDns.forEach { + servers.add(it) + } + + val domesticDns = Utils.getDomesticDnsServers(app.defaultDPreference) + + val agDomain = userRule2Domian(app.defaultDPreference.getPrefString(AppConfig.PREF_V2RAY_ROUTING_AGENT, "")) + if (agDomain.size > 0) { + servers.add(V2rayConfig.DnsBean.ServersBean(remoteDns.first(), 53, agDomain)) + } + + val dirDomain = userRule2Domian(app.defaultDPreference.getPrefString(AppConfig.PREF_V2RAY_ROUTING_DIRECT, "")) + if (dirDomain.size > 0) { + servers.add(V2rayConfig.DnsBean.ServersBean(domesticDns.first(), 53, dirDomain)) + } + + val routingMode = app.defaultDPreference.getPrefString(SettingsActivity.PREF_ROUTING_MODE, "0") + if (routingMode == "2" || routingMode == "3") { + servers.add(V2rayConfig.DnsBean.ServersBean(domesticDns.first(), 53, arrayListOf("geosite:cn"))) + } + + val blkDomain = userRule2Domian(app.defaultDPreference.getPrefString(AppConfig.PREF_V2RAY_ROUTING_BLOCKED, "")) + if (blkDomain.size > 0) { + hosts.putAll(blkDomain.map { it to "127.0.0.1" }) + } + + // hardcode googleapi rule to fix play store problems + hosts.put("domain:googleapis.cn", "googleapis.com") + + // DNS dns对象 + v2rayConfig.dns = V2rayConfig.DnsBean( + servers = servers, + hosts = hosts) + + // DNS inbound对象 + if (v2rayConfig.inbounds.none { e -> e.protocol == "dokodemo-door" && e.tag == "dns-in" }) { + val dnsInboundSettings = V2rayConfig.InboundBean.InSettingsBean( + address = remoteDns.first(), + port = 53, + network = "tcp,udp") + + v2rayConfig.inbounds.add( + V2rayConfig.InboundBean( + tag = "dns-in", + port = 10807, + listen = "127.0.0.1", + protocol = "dokodemo-door", + settings = dnsInboundSettings, + sniffing = null)) + } + + // DNS outbound对象 + if (v2rayConfig.outbounds.none { e -> e.protocol == "dns" && e.tag == "dns-out" }) { + v2rayConfig.outbounds.add( + V2rayConfig.OutboundBean( + protocol = "dns", + tag = "dns-out", + settings = null, + streamSettings = null, + mux = null)) + } + + // DNS routing + v2rayConfig.routing.rules.add(0, V2rayConfig.RoutingBean.RulesBean( + type = "field", + outboundTag = AppConfig.TAG_DIRECT, + port = "53", + ip = domesticDns, + domain = null) + ) + + v2rayConfig.routing.rules.add(0, V2rayConfig.RoutingBean.RulesBean( + type = "field", + outboundTag = AppConfig.TAG_AGENT, + port = "53", + ip = remoteDns, + domain = null) + ) + + // DNS routing tag + v2rayConfig.routing.rules.add(0, V2rayConfig.RoutingBean.RulesBean( + type = "field", + inboundTag = arrayListOf("dns-in"), + outboundTag = "dns-out", + domain = null) + ) + + } catch (e: Exception) { + e.printStackTrace() + return false + } + return true + } + + /** + * Custom Remote Dns + */ + private fun customRemoteDns(vmess: VmessBean, v2rayConfig: V2rayConfig, app: AngApplication): Boolean { + try { + val servers = ArrayList() + + Utils.getRemoteDnsServers(app.defaultDPreference).forEach { + servers.add(it) + } + + v2rayConfig.dns = V2rayConfig.DnsBean(servers = servers) + } catch (e: Exception) { + e.printStackTrace() + return false + } + return true + } + + /** + * is valid config + */ + fun isValidConfig(conf: String): Boolean { + try { + val jObj = JSONObject(conf) + var hasBound = false + //hasBound = (jObj.has("outbounds") and jObj.has("inbounds")) or (jObj.has("outbound") and jObj.has("inbound")) + hasBound = (jObj.has("outbounds")) or (jObj.has("outbound")) + return hasBound + } catch (e: JSONException) { + return false + } + } + + private fun parseDomainName(jsonConfig: String): String { + try { + val jObj = JSONObject(jsonConfig) + var domainName: String + if (jObj.has("outbound")) { + domainName = parseDomainName(jObj.optJSONObject("outbound")) + if (!TextUtils.isEmpty(domainName)) { + return domainName + } + } + if (jObj.has("outbounds")) { + for (i in 0..(jObj.optJSONArray("outbounds").length() - 1)) { + domainName = parseDomainName(jObj.optJSONArray("outbounds").getJSONObject(i)) + if (!TextUtils.isEmpty(domainName)) { + return domainName + } + } + } + if (jObj.has("outboundDetour")) { + for (i in 0..(jObj.optJSONArray("outboundDetour").length() - 1)) { + domainName = parseDomainName(jObj.optJSONArray("outboundDetour").getJSONObject(i)) + if (!TextUtils.isEmpty(domainName)) { + return domainName + } + } + } + } catch (e: Exception) { + e.printStackTrace() + } + return "" + } + + private fun parseDomainName(outbound: JSONObject): String { + try { + if (outbound.has("settings")) { + var vnext: JSONArray? + if (outbound.optJSONObject("settings").has("vnext")) { + // vmess + vnext = outbound.optJSONObject("settings").optJSONArray("vnext") + } else if (outbound.optJSONObject("settings").has("servers")) { + // shadowsocks or socks + vnext = outbound.optJSONObject("settings").optJSONArray("servers") + } else { + return "" + } + for (i in 0..(vnext.length() - 1)) { + val item = vnext.getJSONObject(i) + val address = item.getString("address") + val port = item.getString("port") + if(Utils.isIpv6Address(address)) { + return String.format("[%s]:%s", address, port) + } else { + return String.format("%s:%s", address, port) + } + } + } + } catch (e: Exception) { + e.printStackTrace() + } + return "" + } +} \ No newline at end of file diff --git a/V2rayNG/app/src/main/res/anim/fade_in.xml b/V2rayNG/app/src/main/res/anim/fade_in.xml new file mode 100644 index 000000000..29e04320a --- /dev/null +++ b/V2rayNG/app/src/main/res/anim/fade_in.xml @@ -0,0 +1,6 @@ + + diff --git a/V2rayNG/app/src/main/res/anim/fade_out.xml b/V2rayNG/app/src/main/res/anim/fade_out.xml new file mode 100644 index 000000000..2b8bb1cb1 --- /dev/null +++ b/V2rayNG/app/src/main/res/anim/fade_out.xml @@ -0,0 +1,6 @@ + + diff --git a/V2rayNG/app/src/main/res/color-v21/color_highlight_material.xml b/V2rayNG/app/src/main/res/color-v21/color_highlight_material.xml new file mode 100644 index 000000000..5029d3e96 --- /dev/null +++ b/V2rayNG/app/src/main/res/color-v21/color_highlight_material.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/V2rayNG/app/src/main/res/drawable-xxhdpi/donate.png b/V2rayNG/app/src/main/res/drawable-xxhdpi/donate.png new file mode 100644 index 000000000..8825c5320 Binary files /dev/null and b/V2rayNG/app/src/main/res/drawable-xxhdpi/donate.png differ diff --git a/V2rayNG/app/src/main/res/drawable-xxhdpi/side_nav_bar.xml b/V2rayNG/app/src/main/res/drawable-xxhdpi/side_nav_bar.xml new file mode 100644 index 000000000..6d81870b0 --- /dev/null +++ b/V2rayNG/app/src/main/res/drawable-xxhdpi/side_nav_bar.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/V2rayNG/app/src/main/res/drawable/ic_action_done.xml b/V2rayNG/app/src/main/res/drawable/ic_action_done.xml new file mode 100644 index 000000000..33a117f67 --- /dev/null +++ b/V2rayNG/app/src/main/res/drawable/ic_action_done.xml @@ -0,0 +1,9 @@ + + + diff --git a/V2rayNG/app/src/main/res/drawable/ic_add_white_24dp.xml b/V2rayNG/app/src/main/res/drawable/ic_add_white_24dp.xml new file mode 100644 index 000000000..b9b8eca8b --- /dev/null +++ b/V2rayNG/app/src/main/res/drawable/ic_add_white_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/V2rayNG/app/src/main/res/drawable/ic_attach_money_black_24dp.xml b/V2rayNG/app/src/main/res/drawable/ic_attach_money_black_24dp.xml new file mode 100644 index 000000000..b520fc98d --- /dev/null +++ b/V2rayNG/app/src/main/res/drawable/ic_attach_money_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/V2rayNG/app/src/main/res/drawable/ic_attach_money_white_24dp.xml b/V2rayNG/app/src/main/res/drawable/ic_attach_money_white_24dp.xml new file mode 100644 index 000000000..2b65f0c66 --- /dev/null +++ b/V2rayNG/app/src/main/res/drawable/ic_attach_money_white_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/V2rayNG/app/src/main/res/drawable/ic_close_grey_800_24dp.xml b/V2rayNG/app/src/main/res/drawable/ic_close_grey_800_24dp.xml new file mode 100644 index 000000000..8a9a52263 --- /dev/null +++ b/V2rayNG/app/src/main/res/drawable/ic_close_grey_800_24dp.xml @@ -0,0 +1,10 @@ + + + + diff --git a/V2rayNG/app/src/main/res/drawable/ic_copy_white.xml b/V2rayNG/app/src/main/res/drawable/ic_copy_white.xml new file mode 100644 index 000000000..e50927b22 --- /dev/null +++ b/V2rayNG/app/src/main/res/drawable/ic_copy_white.xml @@ -0,0 +1,9 @@ + + + diff --git a/V2rayNG/app/src/main/res/drawable/ic_delete_black_24dp.xml b/V2rayNG/app/src/main/res/drawable/ic_delete_black_24dp.xml new file mode 100644 index 000000000..2f5557afd --- /dev/null +++ b/V2rayNG/app/src/main/res/drawable/ic_delete_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/V2rayNG/app/src/main/res/drawable/ic_delete_white_24dp.xml b/V2rayNG/app/src/main/res/drawable/ic_delete_white_24dp.xml new file mode 100644 index 000000000..ab38bb6d6 --- /dev/null +++ b/V2rayNG/app/src/main/res/drawable/ic_delete_white_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/V2rayNG/app/src/main/res/drawable/ic_description_black_24dp.xml b/V2rayNG/app/src/main/res/drawable/ic_description_black_24dp.xml new file mode 100644 index 000000000..38c33351c --- /dev/null +++ b/V2rayNG/app/src/main/res/drawable/ic_description_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/V2rayNG/app/src/main/res/drawable/ic_description_white_24dp.xml b/V2rayNG/app/src/main/res/drawable/ic_description_white_24dp.xml new file mode 100644 index 000000000..7e0d28e3e --- /dev/null +++ b/V2rayNG/app/src/main/res/drawable/ic_description_white_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/V2rayNG/app/src/main/res/drawable/ic_edit_black_24dp.xml b/V2rayNG/app/src/main/res/drawable/ic_edit_black_24dp.xml new file mode 100644 index 000000000..2ab2fb753 --- /dev/null +++ b/V2rayNG/app/src/main/res/drawable/ic_edit_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/V2rayNG/app/src/main/res/drawable/ic_fab_check.xml b/V2rayNG/app/src/main/res/drawable/ic_fab_check.xml new file mode 100644 index 000000000..54f825f8f --- /dev/null +++ b/V2rayNG/app/src/main/res/drawable/ic_fab_check.xml @@ -0,0 +1,9 @@ + + + diff --git a/V2rayNG/app/src/main/res/drawable/ic_fab_uncheck.png b/V2rayNG/app/src/main/res/drawable/ic_fab_uncheck.png new file mode 100644 index 000000000..39a104135 Binary files /dev/null and b/V2rayNG/app/src/main/res/drawable/ic_fab_uncheck.png differ diff --git a/V2rayNG/app/src/main/res/drawable/ic_feedback_white_24dp.xml b/V2rayNG/app/src/main/res/drawable/ic_feedback_white_24dp.xml new file mode 100644 index 000000000..3e08fae23 --- /dev/null +++ b/V2rayNG/app/src/main/res/drawable/ic_feedback_white_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/V2rayNG/app/src/main/res/drawable/ic_image_photo.xml b/V2rayNG/app/src/main/res/drawable/ic_image_photo.xml new file mode 100644 index 000000000..c701a63f4 --- /dev/null +++ b/V2rayNG/app/src/main/res/drawable/ic_image_photo.xml @@ -0,0 +1,9 @@ + + + diff --git a/V2rayNG/app/src/main/res/drawable/ic_info_black_24dp.xml b/V2rayNG/app/src/main/res/drawable/ic_info_black_24dp.xml new file mode 100644 index 000000000..34b8202e9 --- /dev/null +++ b/V2rayNG/app/src/main/res/drawable/ic_info_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/V2rayNG/app/src/main/res/drawable/ic_logcat_white_24dp.xml b/V2rayNG/app/src/main/res/drawable/ic_logcat_white_24dp.xml new file mode 100644 index 000000000..e7d3eb313 --- /dev/null +++ b/V2rayNG/app/src/main/res/drawable/ic_logcat_white_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/V2rayNG/app/src/main/res/drawable/ic_qu_scan_black_24dp.xml b/V2rayNG/app/src/main/res/drawable/ic_qu_scan_black_24dp.xml new file mode 100644 index 000000000..a31063ba5 --- /dev/null +++ b/V2rayNG/app/src/main/res/drawable/ic_qu_scan_black_24dp.xml @@ -0,0 +1,19 @@ + + + + + + \ No newline at end of file diff --git a/V2rayNG/app/src/main/res/drawable/ic_qu_settings_black_24dp.xml b/V2rayNG/app/src/main/res/drawable/ic_qu_settings_black_24dp.xml new file mode 100644 index 000000000..01f193f62 --- /dev/null +++ b/V2rayNG/app/src/main/res/drawable/ic_qu_settings_black_24dp.xml @@ -0,0 +1,13 @@ + + + + + diff --git a/V2rayNG/app/src/main/res/drawable/ic_qu_switch_black_24dp.xml b/V2rayNG/app/src/main/res/drawable/ic_qu_switch_black_24dp.xml new file mode 100644 index 000000000..6bc10fd87 --- /dev/null +++ b/V2rayNG/app/src/main/res/drawable/ic_qu_switch_black_24dp.xml @@ -0,0 +1,18 @@ + + + + + + + + \ No newline at end of file diff --git a/V2rayNG/app/src/main/res/drawable/ic_save_white_24dp.xml b/V2rayNG/app/src/main/res/drawable/ic_save_white_24dp.xml new file mode 100644 index 000000000..a7a81a25d --- /dev/null +++ b/V2rayNG/app/src/main/res/drawable/ic_save_white_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/V2rayNG/app/src/main/res/drawable/ic_scan_black_24dp.xml b/V2rayNG/app/src/main/res/drawable/ic_scan_black_24dp.xml new file mode 100644 index 000000000..a31063ba5 --- /dev/null +++ b/V2rayNG/app/src/main/res/drawable/ic_scan_black_24dp.xml @@ -0,0 +1,19 @@ + + + + + + \ No newline at end of file diff --git a/V2rayNG/app/src/main/res/drawable/ic_select_all_white_24dp.xml b/V2rayNG/app/src/main/res/drawable/ic_select_all_white_24dp.xml new file mode 100644 index 000000000..a24c01bfa --- /dev/null +++ b/V2rayNG/app/src/main/res/drawable/ic_select_all_white_24dp.xml @@ -0,0 +1,10 @@ + + + + diff --git a/V2rayNG/app/src/main/res/drawable/ic_settings_white_24dp.xml b/V2rayNG/app/src/main/res/drawable/ic_settings_white_24dp.xml new file mode 100644 index 000000000..ce997a727 --- /dev/null +++ b/V2rayNG/app/src/main/res/drawable/ic_settings_white_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/V2rayNG/app/src/main/res/drawable/ic_share_black_24dp.xml b/V2rayNG/app/src/main/res/drawable/ic_share_black_24dp.xml new file mode 100644 index 000000000..e3fe874d6 --- /dev/null +++ b/V2rayNG/app/src/main/res/drawable/ic_share_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/V2rayNG/app/src/main/res/drawable/ic_share_white_24dp.xml b/V2rayNG/app/src/main/res/drawable/ic_share_white_24dp.xml new file mode 100644 index 000000000..904066636 --- /dev/null +++ b/V2rayNG/app/src/main/res/drawable/ic_share_white_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/V2rayNG/app/src/main/res/drawable/ic_shortcut_background.xml b/V2rayNG/app/src/main/res/drawable/ic_shortcut_background.xml new file mode 100644 index 000000000..61d2d9929 --- /dev/null +++ b/V2rayNG/app/src/main/res/drawable/ic_shortcut_background.xml @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file diff --git a/V2rayNG/app/src/main/res/drawable/ic_start_busy.xml b/V2rayNG/app/src/main/res/drawable/ic_start_busy.xml new file mode 100644 index 000000000..f0154c373 --- /dev/null +++ b/V2rayNG/app/src/main/res/drawable/ic_start_busy.xml @@ -0,0 +1,13 @@ + + + + + diff --git a/V2rayNG/app/src/main/res/drawable/ic_start_connected.xml b/V2rayNG/app/src/main/res/drawable/ic_start_connected.xml new file mode 100644 index 000000000..bc8c36649 --- /dev/null +++ b/V2rayNG/app/src/main/res/drawable/ic_start_connected.xml @@ -0,0 +1,13 @@ + + + + + diff --git a/V2rayNG/app/src/main/res/drawable/ic_start_connected_black.xml b/V2rayNG/app/src/main/res/drawable/ic_start_connected_black.xml new file mode 100644 index 000000000..90385316e --- /dev/null +++ b/V2rayNG/app/src/main/res/drawable/ic_start_connected_black.xml @@ -0,0 +1,13 @@ + + + + + diff --git a/V2rayNG/app/src/main/res/drawable/ic_start_idle.xml b/V2rayNG/app/src/main/res/drawable/ic_start_idle.xml new file mode 100644 index 000000000..ef9b99d3d --- /dev/null +++ b/V2rayNG/app/src/main/res/drawable/ic_start_idle.xml @@ -0,0 +1,24 @@ + + + + + + + + diff --git a/V2rayNG/app/src/main/res/drawable/ic_subscriptions_black_24dp.xml b/V2rayNG/app/src/main/res/drawable/ic_subscriptions_black_24dp.xml new file mode 100644 index 000000000..6f0ed4551 --- /dev/null +++ b/V2rayNG/app/src/main/res/drawable/ic_subscriptions_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/V2rayNG/app/src/main/res/drawable/ic_subscriptions_white_24dp.xml b/V2rayNG/app/src/main/res/drawable/ic_subscriptions_white_24dp.xml new file mode 100644 index 000000000..bc20a83af --- /dev/null +++ b/V2rayNG/app/src/main/res/drawable/ic_subscriptions_white_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/V2rayNG/app/src/main/res/drawable/ic_v.xml b/V2rayNG/app/src/main/res/drawable/ic_v.xml new file mode 100644 index 000000000..cb4930cc4 --- /dev/null +++ b/V2rayNG/app/src/main/res/drawable/ic_v.xml @@ -0,0 +1,10 @@ + + + + diff --git a/V2rayNG/app/src/main/res/drawable/ic_v_connected_black.xml b/V2rayNG/app/src/main/res/drawable/ic_v_connected_black.xml new file mode 100644 index 000000000..0c216c12c --- /dev/null +++ b/V2rayNG/app/src/main/res/drawable/ic_v_connected_black.xml @@ -0,0 +1,10 @@ + + + + diff --git a/V2rayNG/app/src/main/res/drawable/ic_v_idle.xml b/V2rayNG/app/src/main/res/drawable/ic_v_idle.xml new file mode 100644 index 000000000..87f0cca14 --- /dev/null +++ b/V2rayNG/app/src/main/res/drawable/ic_v_idle.xml @@ -0,0 +1,21 @@ + + + + + + + diff --git a/V2rayNG/app/src/main/res/drawable/ic_whatshot_black_24dp.xml b/V2rayNG/app/src/main/res/drawable/ic_whatshot_black_24dp.xml new file mode 100644 index 000000000..1cbc037f7 --- /dev/null +++ b/V2rayNG/app/src/main/res/drawable/ic_whatshot_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/V2rayNG/app/src/main/res/drawable/ic_whatshot_white_24dp.xml b/V2rayNG/app/src/main/res/drawable/ic_whatshot_white_24dp.xml new file mode 100644 index 000000000..ad460f3c3 --- /dev/null +++ b/V2rayNG/app/src/main/res/drawable/ic_whatshot_white_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/V2rayNG/app/src/main/res/drawable/nav_header_bg.png b/V2rayNG/app/src/main/res/drawable/nav_header_bg.png new file mode 100644 index 000000000..d383f6a59 Binary files /dev/null and b/V2rayNG/app/src/main/res/drawable/nav_header_bg.png differ diff --git a/V2rayNG/app/src/main/res/layout/activity_bypass_list.xml b/V2rayNG/app/src/main/res/layout/activity_bypass_list.xml new file mode 100644 index 000000000..1a6c4b8bb --- /dev/null +++ b/V2rayNG/app/src/main/res/layout/activity_bypass_list.xml @@ -0,0 +1,113 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/V2rayNG/app/src/main/res/layout/activity_logcat.xml b/V2rayNG/app/src/main/res/layout/activity_logcat.xml new file mode 100644 index 000000000..06916be12 --- /dev/null +++ b/V2rayNG/app/src/main/res/layout/activity_logcat.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + diff --git a/V2rayNG/app/src/main/res/layout/activity_main.xml b/V2rayNG/app/src/main/res/layout/activity_main.xml new file mode 100644 index 000000000..054ba3ba6 --- /dev/null +++ b/V2rayNG/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,113 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/V2rayNG/app/src/main/res/layout/activity_none.xml b/V2rayNG/app/src/main/res/layout/activity_none.xml new file mode 100644 index 000000000..1aa97aead --- /dev/null +++ b/V2rayNG/app/src/main/res/layout/activity_none.xml @@ -0,0 +1,6 @@ + + + diff --git a/V2rayNG/app/src/main/res/layout/activity_routing_settings.xml b/V2rayNG/app/src/main/res/layout/activity_routing_settings.xml new file mode 100644 index 000000000..15d7d8041 --- /dev/null +++ b/V2rayNG/app/src/main/res/layout/activity_routing_settings.xml @@ -0,0 +1,17 @@ + + + + + + + \ No newline at end of file diff --git a/V2rayNG/app/src/main/res/layout/activity_server.xml b/V2rayNG/app/src/main/res/layout/activity_server.xml new file mode 100644 index 000000000..1630abc0c --- /dev/null +++ b/V2rayNG/app/src/main/res/layout/activity_server.xml @@ -0,0 +1,252 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/V2rayNG/app/src/main/res/layout/activity_server2.xml b/V2rayNG/app/src/main/res/layout/activity_server2.xml new file mode 100644 index 000000000..038d8c12e --- /dev/null +++ b/V2rayNG/app/src/main/res/layout/activity_server2.xml @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/V2rayNG/app/src/main/res/layout/activity_server3.xml b/V2rayNG/app/src/main/res/layout/activity_server3.xml new file mode 100644 index 000000000..3f0af8fbd --- /dev/null +++ b/V2rayNG/app/src/main/res/layout/activity_server3.xml @@ -0,0 +1,129 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/V2rayNG/app/src/main/res/layout/activity_server4.xml b/V2rayNG/app/src/main/res/layout/activity_server4.xml new file mode 100644 index 000000000..821d61ed3 --- /dev/null +++ b/V2rayNG/app/src/main/res/layout/activity_server4.xml @@ -0,0 +1,90 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/V2rayNG/app/src/main/res/layout/activity_settings.xml b/V2rayNG/app/src/main/res/layout/activity_settings.xml new file mode 100644 index 000000000..f674f2ae0 --- /dev/null +++ b/V2rayNG/app/src/main/res/layout/activity_settings.xml @@ -0,0 +1,8 @@ + + \ No newline at end of file diff --git a/V2rayNG/app/src/main/res/layout/activity_sub_edit.xml b/V2rayNG/app/src/main/res/layout/activity_sub_edit.xml new file mode 100644 index 000000000..91f173571 --- /dev/null +++ b/V2rayNG/app/src/main/res/layout/activity_sub_edit.xml @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/V2rayNG/app/src/main/res/layout/activity_sub_setting.xml b/V2rayNG/app/src/main/res/layout/activity_sub_setting.xml new file mode 100644 index 000000000..033da71fc --- /dev/null +++ b/V2rayNG/app/src/main/res/layout/activity_sub_setting.xml @@ -0,0 +1,22 @@ + + + + + + + + + + \ No newline at end of file diff --git a/V2rayNG/app/src/main/res/layout/activity_tasker.xml b/V2rayNG/app/src/main/res/layout/activity_tasker.xml new file mode 100644 index 000000000..f2661896b --- /dev/null +++ b/V2rayNG/app/src/main/res/layout/activity_tasker.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/V2rayNG/app/src/main/res/layout/fragment_routing_settings.xml b/V2rayNG/app/src/main/res/layout/fragment_routing_settings.xml new file mode 100644 index 000000000..815b3a31d --- /dev/null +++ b/V2rayNG/app/src/main/res/layout/fragment_routing_settings.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + diff --git a/V2rayNG/app/src/main/res/layout/item_qrcode.xml b/V2rayNG/app/src/main/res/layout/item_qrcode.xml new file mode 100644 index 000000000..3c53162ad --- /dev/null +++ b/V2rayNG/app/src/main/res/layout/item_qrcode.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/V2rayNG/app/src/main/res/layout/item_recycler_bypass_list.xml b/V2rayNG/app/src/main/res/layout/item_recycler_bypass_list.xml new file mode 100644 index 000000000..2c4031d96 --- /dev/null +++ b/V2rayNG/app/src/main/res/layout/item_recycler_bypass_list.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/V2rayNG/app/src/main/res/layout/item_recycler_footer.xml b/V2rayNG/app/src/main/res/layout/item_recycler_footer.xml new file mode 100644 index 000000000..268f76900 --- /dev/null +++ b/V2rayNG/app/src/main/res/layout/item_recycler_footer.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/V2rayNG/app/src/main/res/layout/item_recycler_main.xml b/V2rayNG/app/src/main/res/layout/item_recycler_main.xml new file mode 100644 index 000000000..3751efd95 --- /dev/null +++ b/V2rayNG/app/src/main/res/layout/item_recycler_main.xml @@ -0,0 +1,178 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/V2rayNG/app/src/main/res/layout/item_recycler_sub_setting.xml b/V2rayNG/app/src/main/res/layout/item_recycler_sub_setting.xml new file mode 100644 index 000000000..30f93c150 --- /dev/null +++ b/V2rayNG/app/src/main/res/layout/item_recycler_sub_setting.xml @@ -0,0 +1,97 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/V2rayNG/app/src/main/res/layout/nav_header.xml b/V2rayNG/app/src/main/res/layout/nav_header.xml new file mode 100644 index 000000000..8eeb79367 --- /dev/null +++ b/V2rayNG/app/src/main/res/layout/nav_header.xml @@ -0,0 +1,24 @@ + + + + + + + + diff --git a/V2rayNG/app/src/main/res/layout/nav_toolbar.xml b/V2rayNG/app/src/main/res/layout/nav_toolbar.xml new file mode 100644 index 000000000..d1a0d9105 --- /dev/null +++ b/V2rayNG/app/src/main/res/layout/nav_toolbar.xml @@ -0,0 +1,15 @@ + + + + + + \ No newline at end of file diff --git a/V2rayNG/app/src/main/res/layout/nav_view.xml b/V2rayNG/app/src/main/res/layout/nav_view.xml new file mode 100644 index 000000000..77b0d6ca3 --- /dev/null +++ b/V2rayNG/app/src/main/res/layout/nav_view.xml @@ -0,0 +1,11 @@ + + \ No newline at end of file diff --git a/V2rayNG/app/src/main/res/layout/widget_switch.xml b/V2rayNG/app/src/main/res/layout/widget_switch.xml new file mode 100644 index 000000000..aa5d03eca --- /dev/null +++ b/V2rayNG/app/src/main/res/layout/widget_switch.xml @@ -0,0 +1,29 @@ + + + + + + + + + + \ No newline at end of file diff --git a/V2rayNG/app/src/main/res/menu/action_server.xml b/V2rayNG/app/src/main/res/menu/action_server.xml new file mode 100644 index 000000000..f231b0564 --- /dev/null +++ b/V2rayNG/app/src/main/res/menu/action_server.xml @@ -0,0 +1,14 @@ + +

+ + + \ No newline at end of file diff --git a/V2rayNG/app/src/main/res/menu/action_sub_setting.xml b/V2rayNG/app/src/main/res/menu/action_sub_setting.xml new file mode 100644 index 000000000..11cb0c1be --- /dev/null +++ b/V2rayNG/app/src/main/res/menu/action_sub_setting.xml @@ -0,0 +1,19 @@ + + + + + + \ No newline at end of file diff --git a/V2rayNG/app/src/main/res/menu/menu_bypass_list.xml b/V2rayNG/app/src/main/res/menu/menu_bypass_list.xml new file mode 100644 index 000000000..dae2bcf1e --- /dev/null +++ b/V2rayNG/app/src/main/res/menu/menu_bypass_list.xml @@ -0,0 +1,15 @@ + + + + + + \ No newline at end of file diff --git a/V2rayNG/app/src/main/res/menu/menu_drawer.xml b/V2rayNG/app/src/main/res/menu/menu_drawer.xml new file mode 100644 index 000000000..54994c7b9 --- /dev/null +++ b/V2rayNG/app/src/main/res/menu/menu_drawer.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/V2rayNG/app/src/main/res/menu/menu_logcat.xml b/V2rayNG/app/src/main/res/menu/menu_logcat.xml new file mode 100644 index 000000000..cabe6c2ee --- /dev/null +++ b/V2rayNG/app/src/main/res/menu/menu_logcat.xml @@ -0,0 +1,9 @@ + + + + \ No newline at end of file diff --git a/V2rayNG/app/src/main/res/menu/menu_main.xml b/V2rayNG/app/src/main/res/menu/menu_main.xml new file mode 100644 index 000000000..445cf0543 --- /dev/null +++ b/V2rayNG/app/src/main/res/menu/menu_main.xml @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/V2rayNG/app/src/main/res/menu/menu_routing.xml b/V2rayNG/app/src/main/res/menu/menu_routing.xml new file mode 100644 index 000000000..3c89797d5 --- /dev/null +++ b/V2rayNG/app/src/main/res/menu/menu_routing.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/V2rayNG/app/src/main/res/menu/menu_scanner.xml b/V2rayNG/app/src/main/res/menu/menu_scanner.xml new file mode 100644 index 000000000..b986bb5a4 --- /dev/null +++ b/V2rayNG/app/src/main/res/menu/menu_scanner.xml @@ -0,0 +1,9 @@ + + + + \ No newline at end of file diff --git a/V2rayNG/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/V2rayNG/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 000000000..036d09bc5 --- /dev/null +++ b/V2rayNG/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/V2rayNG/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/V2rayNG/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 000000000..036d09bc5 --- /dev/null +++ b/V2rayNG/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/V2rayNG/app/src/main/res/mipmap-hdpi/ic_launcher.png b/V2rayNG/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000..e3325fec5 Binary files /dev/null and b/V2rayNG/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/V2rayNG/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png b/V2rayNG/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png new file mode 100644 index 000000000..4fa78dfda Binary files /dev/null and b/V2rayNG/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png differ diff --git a/V2rayNG/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/V2rayNG/app/src/main/res/mipmap-hdpi/ic_launcher_round.png new file mode 100644 index 000000000..53e3a4c12 Binary files /dev/null and b/V2rayNG/app/src/main/res/mipmap-hdpi/ic_launcher_round.png differ diff --git a/V2rayNG/app/src/main/res/mipmap-mdpi/ic_launcher.png b/V2rayNG/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000..73d68777c Binary files /dev/null and b/V2rayNG/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/V2rayNG/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png b/V2rayNG/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png new file mode 100644 index 000000000..bcd78b509 Binary files /dev/null and b/V2rayNG/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png differ diff --git a/V2rayNG/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/V2rayNG/app/src/main/res/mipmap-mdpi/ic_launcher_round.png new file mode 100644 index 000000000..c344863d5 Binary files /dev/null and b/V2rayNG/app/src/main/res/mipmap-mdpi/ic_launcher_round.png differ diff --git a/V2rayNG/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/V2rayNG/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000..0225c9dfc Binary files /dev/null and b/V2rayNG/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/V2rayNG/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png b/V2rayNG/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png new file mode 100644 index 000000000..7329ebc8b Binary files /dev/null and b/V2rayNG/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png differ diff --git a/V2rayNG/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/V2rayNG/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png new file mode 100644 index 000000000..bd041d1ef Binary files /dev/null and b/V2rayNG/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ diff --git a/V2rayNG/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/V2rayNG/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000..9450faa53 Binary files /dev/null and b/V2rayNG/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/V2rayNG/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png b/V2rayNG/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png new file mode 100644 index 000000000..5deddcd7c Binary files /dev/null and b/V2rayNG/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png differ diff --git a/V2rayNG/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/V2rayNG/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png new file mode 100644 index 000000000..dd673eb4d Binary files /dev/null and b/V2rayNG/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ diff --git a/V2rayNG/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/V2rayNG/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000..0666a1246 Binary files /dev/null and b/V2rayNG/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/V2rayNG/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png b/V2rayNG/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png new file mode 100644 index 000000000..6e048168b Binary files /dev/null and b/V2rayNG/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png differ diff --git a/V2rayNG/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/V2rayNG/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png new file mode 100644 index 000000000..c5a9219de Binary files /dev/null and b/V2rayNG/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ diff --git a/V2rayNG/app/src/main/res/raw/licenses.xml b/V2rayNG/app/src/main/res/raw/licenses.xml new file mode 100644 index 000000000..2df2478e9 --- /dev/null +++ b/V2rayNG/app/src/main/res/raw/licenses.xml @@ -0,0 +1,84 @@ + + + Android Compatibility Library v4 + http://source.android.com + Copyright(C) 2008-2011 The Android Open Source Project + Apache Software License 2.0 + + + Android Compatibility Library v7 + http://source.android.com + Copyright(C) 2008-2011 The Android Open Source Project + Apache Software License 2.0 + + + Android Design Library + http://source.android.com + Copyright(C) 2008-2011 The Android Open Source Project + Apache Software License 2.0 + + + Apache Commons Validator + http://commons.apache.org/proper/commons-collections/ + Copyright(C) 2001-2014 The Apache Software Foundation + Apache Software License 2.0 + + + anko + https://github.com/Kotlin/anko + Apache Software License 2.0 + + + Google Gson + https://github.com/google/gson + Copyright 2008-2011 Google Inc. + Apache Software License 2.0 + + + kotlin + http://kotlinlang.org/ + Copyright 2010-2016 JetBrains s.r.o. + Apache Software License 2.0 + + + Logger + https://github.com/orhanobut/logger + Copyright 2015 Orhan Obut + Apache Software License 2.0 + + + LeakCanary + https://github.com/square/leakcanary + Copyright 2015 Square, Inc. + Apache Software License 2.0 + + + ReactiveNetwork + https://github.com/pwittchen/ReactiveNetwork + Copyright 2016 Piotr Wittchen + Apache Software License 2.0 + + + RecyclerItemDecoration + https://github.com/dinuscxj/RecyclerItemDecoration + Copyright 2015-2019 dinus + Apache Software License 2.0 + + + RxKotlin + https://github.com/ReactiveX/RxKotlin + Copyright 2012 Netflix, Inc. + Apache Software License 2.0 + + + RxPermissions + https://github.com/tbruyelle/RxPermissions + Apache Software License 2.0 + + + RecyclerItemDecoration + https://github.com/dinuscxj/RecyclerItemDecoration + Copyright 2015-2019 dinus + Apache Software License 2.0 + + \ No newline at end of file diff --git a/V2rayNG/app/src/main/res/values-v21/strings.xml b/V2rayNG/app/src/main/res/values-v21/strings.xml new file mode 100644 index 000000000..fd9dac4ad --- /dev/null +++ b/V2rayNG/app/src/main/res/values-v21/strings.xml @@ -0,0 +1,4 @@ + + + Set proxy for selected apps + \ No newline at end of file diff --git a/V2rayNG/app/src/main/res/values-v21/styles.xml b/V2rayNG/app/src/main/res/values-v21/styles.xml new file mode 100644 index 000000000..fd7a05843 --- /dev/null +++ b/V2rayNG/app/src/main/res/values-v21/styles.xml @@ -0,0 +1,8 @@ + + + + diff --git a/V2rayNG/app/src/main/res/values-zh-rCN-v21/strings.xml b/V2rayNG/app/src/main/res/values-zh-rCN-v21/strings.xml new file mode 100644 index 000000000..740864c20 --- /dev/null +++ b/V2rayNG/app/src/main/res/values-zh-rCN-v21/strings.xml @@ -0,0 +1,4 @@ + + + 为应用程序分别设置代理 + \ No newline at end of file diff --git a/V2rayNG/app/src/main/res/values-zh-rCN/strings.xml b/V2rayNG/app/src/main/res/values-zh-rCN/strings.xml new file mode 100644 index 000000000..c6e3216d1 --- /dev/null +++ b/V2rayNG/app/src/main/res/values-zh-rCN/strings.xml @@ -0,0 +1,184 @@ + + + v2rayNG + 开关 + 开关 + 初次使用此功能请先用APP激活VPN + Open navigation drawer + Close navigation drawer + + + 停止 + 无法取得权D:\vssHotel\SourceCode\Hotel.root\Hotel\Clubank.Hotel\FrontCounter\CheckoutListForm.cs限 + 点击了解更多 + 启动服务中 + 关闭中 + 启动服务成功 + 启动服务失败 + + + 配置文件 + 添加配置 + 保存配置 + 删除配置 + 扫描二维码 + 从剪贴板导入 + 手动输入[Vmess] + 手动输入[Shadowsocks] + 手动输入[Socks] + 自定义配置 + 从剪贴板导入自定义配置 + 从本地导入自定义配置 + 剪贴板URL导入自定义配置 + 扫描URL导入自定义配置 + 确认删除? + 别名(remarks) + 地址(address) + 端口(port) + 用户ID(id) + 额外ID(alterId) + 加密方式(security) + 传输协议(network) + 功能设置(不清楚则保持默认值) + 伪装类型(type) + 伪装域名host(host/ws host/h2 host)/QUIC 加密方式 + path(ws path/h2 path)/QUIC 加密密钥 + 底层传输安全 + allowInsecure + 服务器地址 + 服务器端口 + 密码 + 加密方式 + 成功 + 失败 + 没有数据 + 不正确的协议 + 解码失败 + 选择一个配置文件 + 请安装一个文件管理器 + 自定义配置 + 无效的配置文件 + 内容 + 剪贴板中没有数据 + 无效的网址 + 确保inbound有socks=10808,http=10809 + + + 正在加载 + 全选 + 输入关键字 + 绕行模式 + 自动选中需代理应用 + 正在下载内容 + + + 设置 + 关于 + 进阶设置 + + 分应用代理 + 分应用代理仅支持 Android 5.0 Lollipop 及更高 + + 启用Mux多路复用 + 开启可能会加速,关闭可能会减少断流 + + 启用速度显示 + 在通知中显示当前速度 + + 启用流量探测 + 流量探测 + + 启用本地DNS + DNS由本地设备的v2Ray-core处理(测试) + + IPv6 路由 + 转发 IPv6 流量到远程服务器(测试) + + 路由设置 + 域名策略 + 路由模式 + 自定义路由 + + 捐赠 + 支持开发者 + 陆续增加一些试验性的进阶功能 + + 远程DNS (可选) + DNS + + 境内DNS (可选,本地DNS模式下生效) + DNS + + SOCKS5代理端口 + SOCKS5代理端口 + + HTTP代理端口 + HTTP代理端口 + + 反馈 + 反馈改进或漏洞至 GitHub + 加入Telegram Group + 未找到Telegram app + + 推广 + 一些推广,点击查看详情(捐赠可去除) + + 版本 + + 初始化错误: + 无法查询到项目 + 错误: + 错误,真实性验证失败. + 感谢您的捐赠! + 捐赠 + Paypal + + Logcat + 复制 + 导出全部配置至剪贴板 + 订阅设置 + 备注 + 地址(url) + 更新订阅 + 测试所有配置延迟(Tcping) + + 启动服务 + 确定 + + 路由设置 + 用逗号(,)隔开,可以一行多个,记得保存 + 保存 + 清空 + 扫描并替换 + 扫描并追加 + 设置默认路由规则 + + "检查网络连接" + "测试中…" + "连接成功:延时 %d 毫秒" + "失败:%s" + "无互联网连接" + "状态码无效(#%d)" + "已连接,点击测试连接" + "未连接" + + + 二维码 + 导出至剪贴板 + 导出完整配置至剪贴板 + + share_method + + 代理的网址或IP + 直连的网址或IP + 阻止的网址或IP + + + + 全局 + 绕过局域网地址 + 绕过大陆地址 + 绕过局域网及大陆地址 + + + diff --git a/V2rayNG/app/src/main/res/values-zh-rTW-v21/strings.xml b/V2rayNG/app/src/main/res/values-zh-rTW-v21/strings.xml new file mode 100644 index 000000000..81da2b40d --- /dev/null +++ b/V2rayNG/app/src/main/res/values-zh-rTW-v21/strings.xml @@ -0,0 +1,4 @@ + + + 為選擇的應用程式設定 Proxy + diff --git a/V2rayNG/app/src/main/res/values-zh-rTW/strings.xml b/V2rayNG/app/src/main/res/values-zh-rTW/strings.xml new file mode 100644 index 000000000..e9c6587e2 --- /dev/null +++ b/V2rayNG/app/src/main/res/values-zh-rTW/strings.xml @@ -0,0 +1,185 @@ + + + v2rayNG + 切換 + 切換 + 首次使用此功能,請使用此應用程式來啟用 VPN + Open navigation drawer + Close navigation drawer + + + 停止 + 無法取得此權限 + 按一下瞭解更多 + 啟動服務 + 停止服務 + 啟動服務成功 + 啟動服務失敗 + + + 組態 + 新增組態 + 儲存組態 + 刪除組態 + 自 QR 碼匯入組態 + 自剪貼簿匯入組態 + 手動鍵入[Vmess] + 手動鍵入[Shadowsocks] + 手動鍵入[Socks] + 自訂組態 + 自剪貼簿匯入自訂組態 + 自本機匯入自訂組態 + 自網址匯入自訂組態 + 掃描網址匯入自訂組態 + 確定刪除? + 備註 + 位址 + 埠 + 使用者識別碼 + AlterId + 安全性 + 網路 + 更多功能 + 標頭類型 + 要求主機(host/ws host/h2 host)/QUIC加密方式 + path(ws path/h2 path)/QUIC加密密鑰 + 傳輸層安全性 + allowInsecure + 伺服器位址 + 伺服器埠 + 密碼 + 加密方式 + 成功 + 失敗 + 無資料 + 通訊協定不正確 + 解碼失敗 + 選取一個設定檔 + 請安裝檔案總管。 + 自訂組態 + 無效組態 + 內容 + 剪貼簿內無資料 + 網址無效 + ​​確保inbound有socks=10808,http=10809 + + + 載入 + 全選 + 輸入關鍵字 + 略過模式 + 自動選中需代理應用 + 正在下載內容 + + + + 設定 + 關於 + 進階設定 + + Proxy 個別應用程式 + Proxy 個別應用程式模式只支援 Android 5.0 (Lollipop) 或更高 + + 啟用 Mux + 啟用或許會加快網路速度,切換或許會閃爍 + + 啟用速度顯示 + 在通知中顯示當前速度 + + 啟用流量探測 + 流量探測 + + 啟用本地DNS + DNS請求由本地的v2ray core處理(測試) + + IPv6 路由 + 向遠端重新導向 IPv6 流量(測試) + + 路由設置 + 域名策略 + 路由模式 + 自訂路由 + + 捐款 + 向開發人員捐款 + 新增一些實驗性進階功能 + + 遠端DNS (可选) + DNS + + 境内DNS (可选,僅本地DNS模式下生效) + DNS + + SOCKS5代理端口 + SOCKS5代理端口 + + HTTP代理端口 + HTTP代理端口 + + + 回饋 + 回饋提升或前往 GitHub 回報 Bug + 加入Telegram Group + 未找到Telegram app + + 推廣 + 一些推廣,點擊查看詳情(捐款可去除) + + 版本 + + 錯誤設定: + Error querying inventory + 購買錯誤: + 購買錯誤,真實性驗證失敗。 + 感謝您的捐款! + 捐款 + Paypal + + Logcat + 複製 + 匯出全部配置至剪貼簿 + 訂閱設定 + 備註 + 位址(url) + 更新訂閱 + 測試所有配置延遲(Tcping) + + 啟動服務 + 確定 + + 路由設定 + 以英文逗號「,」分隔,記得儲存唷 + 儲存 + 清除 + 掃描並取代 + 掃描並附加 + 設置默認路由規則 + + "檢查連線能力" + "測試中……" + "成功: %d 毫秒延遲" + "偵測出網際網路連線失敗: %s" + "無法使用網際網路" + "錯誤碼: (#%d)" + "已連線,輕觸以檢查連線能力" + "未連線" + + + QR 碼 + 匯出至剪貼簿 + 匯出完整配置至剪貼簿 + + + + Proxy 網址或 IP + 直連網址或 IP + 已封鎖的網址或 IP + + + + 全球 + 略過局域網 + 略過中國大陸 + 略過局域網及中國大陸 + + diff --git a/V2rayNG/app/src/main/res/values/arrays.xml b/V2rayNG/app/src/main/res/values/arrays.xml new file mode 100644 index 000000000..03e5d437e --- /dev/null +++ b/V2rayNG/app/src/main/res/values/arrays.xml @@ -0,0 +1,69 @@ + + + + chacha20-poly1305 + aes-128-gcm + auto + none + + + aes-256-cfb + aes-128-cfb + chacha20 + chacha20-ietf + aes-256-gcm + aes-128-gcm + chacha20-poly1305 + chacha20-ietf-poly1305 + + + + tcp + kcp + ws + h2 + quic + + + + none + http + srtp + utp + wechat-video + dtls + wireguard + + + + none + http + + + + none + srtp + utp + wechat-video + dtls + wireguard + + + + + tls + + + + 0 + 1 + 2 + 3 + + + + AsIs + IPIfNonMatch + IPOnDemand + + diff --git a/V2rayNG/app/src/main/res/values/colors.xml b/V2rayNG/app/src/main/res/values/colors.xml new file mode 100644 index 000000000..1e7d71a42 --- /dev/null +++ b/V2rayNG/app/src/main/res/values/colors.xml @@ -0,0 +1,12 @@ + + + #607D8B + #455A64 + #CFD8DC + #9E9E9E + #212121 + #757575 + #FFFFFF + #BDBDBD + #185534 + diff --git a/V2rayNG/app/src/main/res/values/dimens.xml b/V2rayNG/app/src/main/res/values/dimens.xml new file mode 100644 index 000000000..144555196 --- /dev/null +++ b/V2rayNG/app/src/main/res/values/dimens.xml @@ -0,0 +1,15 @@ + + + 50dp + 16dp + 16dp + 50dp + 24dp + 72dp + 60dp + + 16dp + 16dp + 8dp + 160dp + \ No newline at end of file diff --git a/V2rayNG/app/src/main/res/values/ic_launcher_background.xml b/V2rayNG/app/src/main/res/values/ic_launcher_background.xml new file mode 100644 index 000000000..6ddd41a2b --- /dev/null +++ b/V2rayNG/app/src/main/res/values/ic_launcher_background.xml @@ -0,0 +1,4 @@ + + + #DEDEDE + \ No newline at end of file diff --git a/V2rayNG/app/src/main/res/values/strings.xml b/V2rayNG/app/src/main/res/values/strings.xml new file mode 100644 index 000000000..8eb7a93d3 --- /dev/null +++ b/V2rayNG/app/src/main/res/values/strings.xml @@ -0,0 +1,185 @@ + + + v2rayNG + Switch + Switch + First use of this feature, please use the app to activate VPN + Open navigation drawer + Close navigation drawer + + + Stop + Unable to obtain the permission + click for more + Start Services + Stop Services + Start Services Success + Start Services Failure + + + Configuration file + Add config + Save config + Delete config + Import config from QRcode + Import config from Clipboard + Type manually[Vmess] + Type manually[Shadowsocks] + Type manually[Socks] + custom config + Import custom config from Clipboard + Import custom config from locally + Import custom config from URL + Import custom config scan URL + Confirm delete? + remarks + address + port + id + alterId + security + network + more function + head type + request host(host/ws host/h2 host)/QUIC securty + path(ws path/h2 path)/QUIC key + tls + allowInsecure + address + port + password + security + Success + Failure + There is nothing + Incorrect protocol + Decoding failed + Select a Config File + Please install a File Manager. + Customize Config + Invalid Config + Content + There is no data in the clipboard + Invalid URL + Ensure inbound has socks=10808,http=10809 + + + Loading + Select all + Enter keywords + Bypass Mode + Auto select proxy app + Downloading content + + + + Settings + About + Advanced Settings + + Per-app proxy + Per-app proxy mode only support Android 5.0 Lollipop or higher + + Enable Mux + Enable maybe speed up network and switch network maybe flash + + Enable speed display + Display current speed in the notification + + Enable Sniffing + Sniffing + + Enable local DNS + DNS processed by v2ray-core (BETA) + + IPv6 Route + Redirect IPv6 traffic to remote (BETA) + + Routing + Domain strategy + Routing mode + Custom routing + + Donate + Donate developer + Add some experimental advanced features + + Remote DNS (Optional) + DNS + + Domestic DNS (Optional, effects on LocalDNS mode) + DNS + + SOCKS5 proxy port + SOCKS5 proxy port + + HTTP proxy port + HTTP proxy port + + Feedback + Feedback enhancements or bugs to GitHub + Join Telegram Group + Telegram app not found + + Promotion + Promotion,click for details(Donation can be removed) + + Version + + Error Setup: + Error querying inventory + Error Purchase: + Error Purchase,Authenticity verification failed. + Thank you for your donation! + Donate + Paypal + + Logcat + Copy + Export all config to clipboard + Subscription setting + remarks + url + Update subscription + Tcping all configuration + + Start Service + Confirm + + Routing Settings + Separated by commas(,),remember to save + Save + Clear + Scan and replace + Scan and append + set default routing rules + + Check Connectivity + Testing… + Success: HTTPS handshake took %dms + Fail to detect internet connection: %s + Internet Unavailable + Error code: #%d + Connected, tap to check connection + Not connected + + + QRcode + Export to clipboard + Export full configuration to clipboard + + + + proxy URL or IP + direct URL or IP + blocked URL or IP + + + + Global + Bypassing the LAN address + Bypass mainland address + Bypassing LAN and mainland address + + + diff --git a/V2rayNG/app/src/main/res/values/styles.xml b/V2rayNG/app/src/main/res/values/styles.xml new file mode 100644 index 000000000..be3498ae1 --- /dev/null +++ b/V2rayNG/app/src/main/res/values/styles.xml @@ -0,0 +1,20 @@ + + + + + + + +