Compare commits
24 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f25aaf43d7 | |||
| 3b365533fc | |||
| a982327030 | |||
| 6ce12b2ea5 | |||
| d7f3ed5182 | |||
| f519103b3e | |||
| d7c40ee883 | |||
| 809d50689b | |||
| 042def7e83 | |||
| 8bb0d6b175 | |||
| 3ae76fab72 | |||
| 46a63e8704 | |||
| 12f8b1584c | |||
| 2b571e2a94 | |||
| ba30697309 | |||
| 879e25bb82 | |||
| 17c858d24c | |||
| 71a6531835 | |||
| 5d760382d7 | |||
| 4dda36673a | |||
| 5aca55cb8f | |||
| 82f14f0d07 | |||
| c0e968ac34 | |||
| 29d3fb710b |
@@ -1,5 +0,0 @@
|
|||||||
blank_issues_enabled: false
|
|
||||||
contact_links:
|
|
||||||
- name: V2Ray程序问题
|
|
||||||
url: https://github.com/v2fly/v2ray-core/
|
|
||||||
about: 如果您有V2Ray而非v2rayNG的问题,请至这个链接讨论。
|
|
||||||
@@ -1,8 +1,3 @@
|
|||||||
---
|
|
||||||
name: v2rayNG程序问题
|
|
||||||
about: 创建一个报告来帮助我们改进
|
|
||||||
---
|
|
||||||
|
|
||||||
在提出问题前请先自行排除服务器端问题,同时也请通过搜索确认是否有人提出过相同问题。
|
在提出问题前请先自行排除服务器端问题,同时也请通过搜索确认是否有人提出过相同问题。
|
||||||
|
|
||||||
|
|
||||||
@@ -19,8 +14,7 @@ about: 创建一个报告来帮助我们改进
|
|||||||
|
|
||||||
### 日志信息
|
### 日志信息
|
||||||
<details>
|
<details>
|
||||||
|
通过 `adb logcat -s com.v2ray.ang GoLog V2rayConfigUtilGoLog Main` 获取日志。请自行删减日志中可能出现的敏感信息。
|
||||||
通过`adb logcat -s com.v2ray.ang GoLog V2rayConfigUtilGoLog Main`获取日志。请自行删减日志中可能出现的敏感信息。
|
|
||||||
|
|
||||||
如果问题可重现,建议先执行`adb logcat -c`清空系统日志再执行上述命令,再操作重现问题。
|
如果问题可重现,建议先执行`adb logcat -c`清空系统日志再执行上述命令,再操作重现问题。
|
||||||
```
|
```
|
||||||
@@ -1,93 +0,0 @@
|
|||||||
name: Build APK
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
workflow_dispatch:
|
|
||||||
inputs:
|
|
||||||
XRAY_CORE_VERSION:
|
|
||||||
description: 'Xray core version or commit hash'
|
|
||||||
required: false
|
|
||||||
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Setup Java
|
|
||||||
uses: actions/setup-java@v4
|
|
||||||
with:
|
|
||||||
distribution: 'temurin'
|
|
||||||
java-version: '21'
|
|
||||||
|
|
||||||
- name: Setup Golang
|
|
||||||
uses: actions/setup-go@v5
|
|
||||||
with:
|
|
||||||
go-version: '1.23.2'
|
|
||||||
cache: false
|
|
||||||
|
|
||||||
- name: Patch Go use 600296
|
|
||||||
#https://go-review.googlesource.com/c/go/+/600296
|
|
||||||
run: |
|
|
||||||
cd "$(go env GOROOT)"
|
|
||||||
curl "https://go-review.googlesource.com/changes/go~600296/revisions/5/patch" | base64 -d | patch --verbose -p 1
|
|
||||||
|
|
||||||
- name: Install gomobile
|
|
||||||
run: |
|
|
||||||
go install golang.org/x/mobile/cmd/gomobile@v0.0.0-20240806205939-81131f6468ab
|
|
||||||
echo "$(go env GOPATH)/bin" >> $GITHUB_PATH
|
|
||||||
|
|
||||||
- name: Setup Android environment
|
|
||||||
uses: android-actions/setup-android@v3
|
|
||||||
|
|
||||||
- name: Build dependencies
|
|
||||||
run: |
|
|
||||||
mkdir ${{ github.workspace }}/build
|
|
||||||
cd ${{ github.workspace }}/build
|
|
||||||
git clone --depth=1 -b main https://github.com/2dust/AndroidLibXrayLite.git
|
|
||||||
cd AndroidLibXrayLite
|
|
||||||
go get github.com/xtls/xray-core@${{ github.event.inputs.XRAY_CORE_VERSION }} || true
|
|
||||||
gomobile init
|
|
||||||
go mod tidy -v
|
|
||||||
gomobile bind -v -androidapi 21 -ldflags='-s -w' ./
|
|
||||||
cp *.aar ${{ github.workspace }}/V2rayNG/app/libs/
|
|
||||||
|
|
||||||
- name: Build APK
|
|
||||||
run: |
|
|
||||||
cd ${{ github.workspace }}/V2rayNG
|
|
||||||
chmod 755 gradlew
|
|
||||||
./gradlew assembleDebug
|
|
||||||
|
|
||||||
- name: Upload arm64-v8a APK
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
if: ${{ success() }}
|
|
||||||
with:
|
|
||||||
name: arm64-v8a
|
|
||||||
path: ${{ github.workspace }}/V2rayNG/app/build/outputs/apk/debug/*arm64-v8a*.apk
|
|
||||||
|
|
||||||
- name: Upload armeabi-v7a APK
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
if: ${{ success() }}
|
|
||||||
with:
|
|
||||||
name: armeabi-v7a
|
|
||||||
path: ${{ github.workspace }}/V2rayNG/app/build/outputs/apk/debug/*armeabi-v7a*.apk
|
|
||||||
|
|
||||||
- name: Upload x86 APK
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
if: ${{ success() }}
|
|
||||||
with:
|
|
||||||
name: x86-apk
|
|
||||||
path: ${{ github.workspace }}/V2rayNG/app/build/outputs/apk/debug/*x86*.apk
|
|
||||||
|
|
||||||
- name: Upload Other APKs
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: others-apk
|
|
||||||
path: |
|
|
||||||
${{ github.workspace }}/V2rayNG/app/build/outputs/apk/debug
|
|
||||||
!${{ github.workspace }}/V2rayNG/app/build/outputs/apk/debug/*arm64-v8a*.apk
|
|
||||||
!${{ github.workspace }}/V2rayNG/app/build/outputs/apk/debug/*armeabi-v7a*.apk
|
|
||||||
!${{ github.workspace }}/V2rayNG/app/build/outputs/apk/debug/*x86*.apk
|
|
||||||
+9
-3
@@ -1,5 +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
|
*.dat
|
||||||
*.jks
|
*.jks
|
||||||
V2rayNG/app/release/output.json
|
V2rayNG/gradle/wrapper/gradle-wrapper.properties
|
||||||
.idea/
|
V2rayNG/gradle/wrapper/gradle-wrapper.properties
|
||||||
.gradle/
|
V2rayNG/app/release/output.json
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -1,20 +1 @@
|
|||||||
# AndroidLibV2rayLite
|
# AndroidLibV2rayLite
|
||||||
|
|
||||||
### Preparation
|
|
||||||
- latest Ubuntu environment
|
|
||||||
- At lease 30G free space
|
|
||||||
- Get Repo [AndroidLibV2rayLite](https://github.com/2dust/AndroidLibV2rayLite) or [AndroidLibXrayLite](https://github.com/2dust/AndroidLibXrayLite)
|
|
||||||
### Prepare Go
|
|
||||||
- Go to https://golang.org/doc/install and install latest go
|
|
||||||
- Make sure `go version` works as expected
|
|
||||||
### Prepare gomobile
|
|
||||||
- Go to https://pkg.go.dev/golang.org/x/mobile/cmd/gomobile and install gomobile
|
|
||||||
- export PATH=$PATH:~/go/bin
|
|
||||||
- Make sure `gomobile init` works as expected
|
|
||||||
### Prepare NDK
|
|
||||||
- Go to https://developer.android.com/ndk/downloads and install latest NDK
|
|
||||||
- export PATH=$PATH:<wherever you ndk is located>
|
|
||||||
- Make sure `ndk-build -v` works as expected
|
|
||||||
### Make
|
|
||||||
- sudo apt install make
|
|
||||||
- Read and understand [build script](https://github.com/2dust/AndroidLibV2rayLite/blob/master/Makefile)
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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())
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
package libv2ray
|
||||||
|
|
||||||
|
//go:generate make all
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
readme.txt
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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*
|
||||||
@@ -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()
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
|
||||||
@@ -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
|
||||||
@@ -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)}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,40 +1,29 @@
|
|||||||
**v2rayNG 隐私权政策**
|
v2rayNG 隐私条款
|
||||||
|
|
||||||
本政策自2023年11月17日起施行
|
最后更新 2017-11-22
|
||||||
|
|
||||||
2dust 将 v2rayNG 应用程序构建为开源应用程序。 本服务由 2dust 免费提供,并且旨在按原样使用。
|
v2rayNG 尊重并保护所有用户的个人隐私权,为此我们向大众公开这份隐私条款。**您使用 v2rayNG 即代表您以阅读并同意了这份条款,如果您不同意这份条款请立即停止使用并卸载 v2rayNG。**
|
||||||
|
|
||||||
v2rayNG 尊重并保护所有用户的个人隐私权,为此我们向大众公开这份隐私权政策。**您使用 v2rayNG 即代表您以阅读并同意了这份条款,如果您不同意这份条款请立即停止使用并卸载 v2rayNG。**
|
1. 信息收集
|
||||||
|
|
||||||
**信息收集**
|
v2rayNG 软件自身不会发送任何信息到开发者,但是您下载软件的应用市场(如 Google Play)可能会收集关于应用运行状态的相关信息并提供给 v2rayNG 开发者。有关这些信息,请阅读您使用的应用市场所提供的隐私条款。
|
||||||
|
|
||||||
v2rayNG 软件自身不会发送任何信息到开发者,但是您下载软件的应用市场(如 Google Play)可能会收集关于应用运行状态的相关信息并提供给 v2rayNG 开发者。有关这些信息,请阅读您使用的应用市场所提供的隐私权政策。
|
v2rayNG 软件中可能包含需要通过 IAP 支付解锁的功能,您的支付信息将由相关的 IAP 渠道进行处理,而我们对支付信息没有访问权。
|
||||||
|
|
||||||
v2rayNG 软件中可能包含需要通过 IAP 支付解锁的功能,您的支付信息将由相关的 IAP 渠道进行处理,而我们对支付信息没有访问权。
|
当您向 v2rayNG 开发者反馈软件运行中的错误时,开发者可能会要求您提供软件以及系统的日志以帮助确认问题的原因。因日志中可能包括敏感信息,此类信息只能由您自己操作发送。**我们不对任何传输服务的安全性和隐私性做任何明示或暗示的担保,请您在传送相关信息时选择可以您自身可以接受的方式。**
|
||||||
|
|
||||||
当您向 v2rayNG 开发者反馈软件运行中的错误时,开发者可能会要求您提供软件以及系统的日志以帮助确认问题的原因。因日志中可能包括敏感信息,此类信息只能由您自己操作发送。**我们不对任何传输服务的安全性和隐私性做任何明示或暗示的担保,请您在传送相关信息时选择可以您自身可以接受的方式。**
|
2. 信息共享
|
||||||
|
|
||||||
**信息共享**
|
我们不会向任何第三方出售收集到的用户数据。我们可能向外部开发者提供信息以协助软件的开发,但是在提供信息之前我们会传达相关保密义务并确定其可以遵守。
|
||||||
|
|
||||||
我们不会向任何第三方出售收集到的用户数据。我们可能向外部开发者提供信息以协助软件的开发,但是在提供信息之前我们会传达相关保密义务并确定其可以遵守。
|
3. 信息存留
|
||||||
|
|
||||||
**信息存留**
|
除非有相关法律规定,我们会在 30 天内清除不需要继续使用的用户数据,或将统计数据整合为无法识别单个用户的综合报告。
|
||||||
|
|
||||||
除非有相关法律规定,我们会在 30 天内清除不需要继续使用的用户数据,或将统计数据整合为无法识别单个用户的综合报告。
|
4. 信息泄露
|
||||||
|
|
||||||
**信息泄露**
|
我们会使用合理的技术和安全手段尽力保护用户的数据,但是无法保证数据的绝对安全。如果我们确认数据发生了泄露,我们会在 7 天内通过可用的渠道通知用户。**您同意不向我们追责任何因不可抗力而造成的损失。**
|
||||||
|
|
||||||
我们会使用合理的技术和安全手段尽力保护用户的数据,但是无法保证数据的绝对安全。如果我们确认数据发生了泄露,我们会在 7 天内通过可用的渠道通知用户。**您同意不向我们追责任何因不可抗力而造成的损失。**
|
5. 条款修改
|
||||||
|
|
||||||
**儿童隐私**
|
|
||||||
|
|
||||||
这些服务不针对 13 岁以下的任何人。我不会故意收集 13 岁以下儿童的个人身份信息。 如果我发现 13 岁以下的儿童向我提供了个人信息,我会立即从我们的服务器中删除该信息。 如果您是父母或监护人,并且您知道您的孩子向我们提供了个人信息,请与我联系,以便我能够采取必要的行动。
|
|
||||||
|
|
||||||
**条款修改**
|
|
||||||
|
|
||||||
我们保留修改这份隐私权政策的权利,但是会确保在更新条款前至少 30 天通过我们的可用渠道和应用内提示来通知用户。**在新条款生效后继续使用软件即表示您同意修改后的隐私权政策。**
|
|
||||||
|
|
||||||
**联系我们**
|
|
||||||
|
|
||||||
如果您对我的隐私政策有任何疑问或建议,请随时通过 CaptainIronng@protonmail.com 与我联系。
|
|
||||||
|
|
||||||
|
我们保留修改这份隐私条款的权利,但是会确保在更新条款前至少 30 天通过我们的可用渠道和应用内提示来通知用户。**在新条款生效后继续使用软件即表示您同意修改后的隐私条款。**
|
||||||
|
|||||||
@@ -1,36 +1,6 @@
|
|||||||
# v2rayNG
|
# v2rayNG
|
||||||
|
|
||||||
A V2Ray client for Android, support [Xray core](https://github.com/XTLS/Xray-core) and [v2fly core](https://github.com/v2fly/v2ray-core)
|
|
||||||
|
|
||||||
[](https://developer.android.com/about/versions/lollipop)
|
|
||||||
[](https://kotlinlang.org)
|
|
||||||
[](https://github.com/2dust/v2rayNG/commits/master)
|
|
||||||
[](https://www.codefactor.io/repository/github/2dust/v2rayng)
|
|
||||||
[](https://github.com/2dust/v2rayNG/releases)
|
|
||||||
[](https://t.me/v2rayn)
|
|
||||||
|
|
||||||
<a href="https://play.google.com/store/apps/details?id=com.v2ray.ang">
|
<a href="https://play.google.com/store/apps/details?id=com.v2ray.ang">
|
||||||
<img alt="Get it on Google Play" src="https://play.google.com/intl/en_us/badges/images/generic/en_badge_web_generic.png" width="165" height="64" />
|
<img alt="Get it on Google Play" src="https://play.google.com/intl/en_us/badges/images/generic/en_badge_web_generic.png" width="165" height="64" />
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
### Telegram Channel
|
|
||||||
[github_2dust](https://t.me/github_2dust)
|
|
||||||
|
|
||||||
### Usage
|
|
||||||
|
|
||||||
#### Geoip and Geosite
|
|
||||||
- geoip.dat and geosite.dat files are in `Android/data/com.v2ray.ang/files/assets` (path may differ on some Android device)
|
|
||||||
- download feature will get enhanced version in this [repo](https://github.com/Loyalsoldier/v2ray-rules-dat) (Note it need a working proxy)
|
|
||||||
- latest official [domain list](https://github.com/v2fly/domain-list-community) and [ip list](https://github.com/v2fly/geoip) can be imported manually
|
|
||||||
- possible to use third party dat file in the same folder, like [h2y](https://guide.v2fly.org/routing/sitedata.html#%E5%A4%96%E7%BD%AE%E7%9A%84%E5%9F%9F%E5%90%8D%E6%96%87%E4%BB%B6)
|
|
||||||
|
|
||||||
### More in our [wiki](https://github.com/2dust/v2rayNG/wiki)
|
|
||||||
|
|
||||||
### Development guide
|
|
||||||
|
|
||||||
Android project under V2rayNG folder can be compiled directly in Android Studio, or using Gradle wrapper. But the v2ray core inside the aar is (probably) outdated.
|
|
||||||
The aar can be compiled from the Golang project [AndroidLibV2rayLite](https://github.com/2dust/AndroidLibV2rayLite) or [AndroidLibXrayLite](https://github.com/2dust/AndroidLibXrayLite).
|
|
||||||
For a quick start, read guide for [Go Mobile](https://github.com/golang/go/wiki/Mobile) and [Makefiles for Go Developers](https://tutorialedge.net/golang/makefiles-for-go-developers/)
|
|
||||||
|
|
||||||
v2rayNG can run on Android Emulators. For WSA, VPN permission need to be granted via
|
|
||||||
`appops set [package name] ACTIVATE_VPN allow`
|
|
||||||
|
|||||||
@@ -7,4 +7,3 @@
|
|||||||
/captures
|
/captures
|
||||||
*.apk
|
*.apk
|
||||||
signing.properties
|
signing.properties
|
||||||
*.aar
|
|
||||||
|
|||||||
@@ -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'
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,134 +0,0 @@
|
|||||||
plugins {
|
|
||||||
id("com.android.application")
|
|
||||||
id("org.jetbrains.kotlin.android")
|
|
||||||
}
|
|
||||||
|
|
||||||
android {
|
|
||||||
namespace = "com.v2ray.ang"
|
|
||||||
compileSdk = 34
|
|
||||||
|
|
||||||
defaultConfig {
|
|
||||||
applicationId = "com.v2ray.ang"
|
|
||||||
minSdk = 21
|
|
||||||
targetSdk = 35
|
|
||||||
versionCode = 607
|
|
||||||
versionName = "1.9.12"
|
|
||||||
multiDexEnabled = true
|
|
||||||
splits {
|
|
||||||
abi {
|
|
||||||
isEnable = true
|
|
||||||
include(
|
|
||||||
"arm64-v8a",
|
|
||||||
"armeabi-v7a",
|
|
||||||
"x86_64",
|
|
||||||
"x86"
|
|
||||||
)
|
|
||||||
isUniversalApk = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
compileOptions {
|
|
||||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
|
||||||
targetCompatibility = JavaVersion.VERSION_1_8
|
|
||||||
}
|
|
||||||
buildTypes {
|
|
||||||
release {
|
|
||||||
isMinifyEnabled = false
|
|
||||||
|
|
||||||
}
|
|
||||||
debug {
|
|
||||||
isMinifyEnabled = false
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
sourceSets {
|
|
||||||
getByName("main") {
|
|
||||||
jniLibs.srcDirs("libs")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
kotlinOptions {
|
|
||||||
jvmTarget = JavaVersion.VERSION_1_8.toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
applicationVariants.all {
|
|
||||||
val variant = this
|
|
||||||
val versionCodes =
|
|
||||||
mapOf("armeabi-v7a" to 4, "arm64-v8a" to 4, "x86" to 4, "x86_64" to 4, "universal" to 4)
|
|
||||||
|
|
||||||
variant.outputs
|
|
||||||
.map { it as com.android.build.gradle.internal.api.ApkVariantOutputImpl }
|
|
||||||
.forEach { output ->
|
|
||||||
val abi = if (output.getFilter("ABI") != null)
|
|
||||||
output.getFilter("ABI")
|
|
||||||
else
|
|
||||||
"universal"
|
|
||||||
|
|
||||||
output.outputFileName = "v2rayNG_${variant.versionName}_${abi}.apk"
|
|
||||||
if (versionCodes.containsKey(abi)) {
|
|
||||||
output.versionCodeOverride = (1000000 * versionCodes[abi]!!).plus(variant.versionCode)
|
|
||||||
} else {
|
|
||||||
return@forEach
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
buildFeatures {
|
|
||||||
viewBinding = true
|
|
||||||
buildConfig = true
|
|
||||||
}
|
|
||||||
|
|
||||||
packaging {
|
|
||||||
jniLibs {
|
|
||||||
useLegacyPackaging = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.aar", "*.jar"))))
|
|
||||||
testImplementation(libs.junit)
|
|
||||||
testImplementation(libs.org.mockito.mockito.inline)
|
|
||||||
testImplementation(libs.mockito.kotlin)
|
|
||||||
|
|
||||||
implementation(libs.flexbox)
|
|
||||||
// Androidx
|
|
||||||
implementation(libs.constraintlayout)
|
|
||||||
implementation(libs.legacy.support.v4)
|
|
||||||
implementation(libs.appcompat)
|
|
||||||
implementation(libs.material)
|
|
||||||
implementation(libs.cardview)
|
|
||||||
implementation(libs.preference.ktx)
|
|
||||||
implementation(libs.recyclerview)
|
|
||||||
implementation(libs.fragment.ktx)
|
|
||||||
implementation(libs.multidex)
|
|
||||||
implementation(libs.viewpager2)
|
|
||||||
|
|
||||||
// Androidx ktx
|
|
||||||
implementation(libs.activity.ktx)
|
|
||||||
implementation(libs.lifecycle.viewmodel.ktx)
|
|
||||||
implementation(libs.lifecycle.livedata.ktx)
|
|
||||||
implementation(libs.lifecycle.runtime.ktx)
|
|
||||||
|
|
||||||
//kotlin
|
|
||||||
implementation(libs.kotlin.reflect)
|
|
||||||
implementation(libs.kotlinx.coroutines.core)
|
|
||||||
implementation(libs.kotlinx.coroutines.android)
|
|
||||||
|
|
||||||
implementation(libs.mmkv.static)
|
|
||||||
implementation(libs.gson)
|
|
||||||
implementation(libs.rxjava)
|
|
||||||
implementation(libs.rxandroid)
|
|
||||||
implementation(libs.rxpermissions)
|
|
||||||
implementation(libs.toastcompat)
|
|
||||||
implementation(libs.editorkit)
|
|
||||||
implementation(libs.language.base)
|
|
||||||
implementation(libs.language.json)
|
|
||||||
implementation(libs.quickie.bundled)
|
|
||||||
implementation(libs.core)
|
|
||||||
implementation(libs.work.runtime.ktx)
|
|
||||||
implementation(libs.work.multiprocess)
|
|
||||||
}
|
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
https://github.com/2dust/v2rayNG/tree/master/AndroidLibV2rayLite
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Vendored
+61
@@ -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.** { *;}
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package com.v2ray.ang;
|
||||||
|
|
||||||
|
import android.app.Application;
|
||||||
|
import android.test.ApplicationTestCase;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <a href="http://d.android.com/tools/testing/testing_android.html">Testing Fundamentals</a>
|
||||||
|
*/
|
||||||
|
public class ApplicationTest extends ApplicationTestCase<Application> {
|
||||||
|
public ApplicationTest() {
|
||||||
|
super(Application.class);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<resources>
|
|
||||||
<item name="app_name" type="string">v2rayNG (DEV)</item>
|
|
||||||
</resources>
|
|
||||||
@@ -1,72 +1,38 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
package="com.v2ray.ang">
|
||||||
tools:ignore="MissingLeanbackLauncher">
|
|
||||||
|
|
||||||
<supports-screens
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
|
||||||
android:anyDensity="true"
|
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE"/>
|
||||||
android:largeScreens="true"
|
|
||||||
android:normalScreens="true"
|
|
||||||
android:smallScreens="true"
|
|
||||||
android:xlargeScreens="true" />
|
|
||||||
|
|
||||||
<uses-sdk
|
|
||||||
android:minSdkVersion="21"
|
|
||||||
tools:overrideLibrary="com.blacksquircle.ui.editorkit" />
|
|
||||||
|
|
||||||
<uses-feature
|
|
||||||
android:name="android.hardware.camera"
|
|
||||||
android:required="false" />
|
|
||||||
<uses-feature
|
|
||||||
android:name="android.hardware.camera.autofocus"
|
|
||||||
android:required="false" />
|
|
||||||
<uses-feature
|
|
||||||
android:name="android.software.leanback"
|
|
||||||
android:required="false" />
|
|
||||||
<uses-feature
|
|
||||||
android:name="android.hardware.touchscreen"
|
|
||||||
android:required="false" />
|
|
||||||
|
|
||||||
<!-- https://developer.android.com/about/versions/11/privacy/package-visibility -->
|
|
||||||
<uses-permission
|
|
||||||
android:name="android.permission.QUERY_ALL_PACKAGES"
|
|
||||||
tools:ignore="QueryAllPackagesPermission" />
|
|
||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
|
||||||
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||||
<!-- <useapplications-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" /> -->
|
<!-- <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" /> -->
|
||||||
<uses-permission android:name="android.permission.CAMERA" />
|
<uses-permission android:name="android.permission.CAMERA" />
|
||||||
|
<uses-permission android:name="com.android.vending.BILLING" />
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
<uses-permission
|
|
||||||
android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE"
|
|
||||||
android:minSdkVersion="34" />
|
|
||||||
<!-- <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> -->
|
<!-- <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> -->
|
||||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
|
||||||
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
|
|
||||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:name=".AngApplication"
|
android:name=".AngApplication"
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
android:banner="@mipmap/ic_banner"
|
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/AppThemeDayNight"
|
android:theme="@style/AppTheme">
|
||||||
android:usesCleartextTraffic="true"
|
|
||||||
tools:targetApi="m">
|
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.MainActivity"
|
android:name=".ui.MainActivity"
|
||||||
android:exported="true"
|
android:theme="@style/AppTheme.NoActionBar"
|
||||||
android:launchMode="singleTask"
|
android:launchMode="singleTask">
|
||||||
android:theme="@style/AppThemeDayNight.NoActionBar">
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
</intent-filter>
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.SEND" />
|
||||||
|
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
|
||||||
|
<data android:mimeType="text/plain" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.service.quicksettings.action.QS_TILE_PREFERENCES" />
|
<action android:name="android.service.quicksettings.action.QS_TILE_PREFERENCES" />
|
||||||
@@ -78,81 +44,34 @@
|
|||||||
</activity>
|
</activity>
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.ServerActivity"
|
android:name=".ui.ServerActivity"
|
||||||
android:exported="false"
|
|
||||||
android:windowSoftInputMode="stateUnchanged" />
|
android:windowSoftInputMode="stateUnchanged" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.ServerCustomConfigActivity"
|
android:name=".ui.Server2Activity"
|
||||||
android:exported="false"
|
|
||||||
android:windowSoftInputMode="stateUnchanged" />
|
android:windowSoftInputMode="stateUnchanged" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.SettingsActivity"
|
android:name=".ui.Server3Activity"
|
||||||
android:exported="false" />
|
android:windowSoftInputMode="stateUnchanged" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.PerAppProxyActivity"
|
android:name=".ui.Server4Activity"
|
||||||
android:exported="false" />
|
android:windowSoftInputMode="stateUnchanged" />
|
||||||
|
<activity android:name=".ui.SettingsActivity" />
|
||||||
|
<activity android:name=".ui.PerAppProxyActivity" />
|
||||||
|
<activity android:name=".ui.ScannerActivity" />
|
||||||
|
<!-- <activity android:name=".InappBuyActivity" />-->
|
||||||
|
<activity android:name=".ui.LogcatActivity" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.ScannerActivity"
|
android:name=".ui.RoutingSettingsActivity"
|
||||||
android:exported="false" />
|
android:windowSoftInputMode="stateUnchanged" />
|
||||||
<activity
|
<activity android:name=".ui.SubSettingActivity" />
|
||||||
android:name=".ui.LogcatActivity"
|
|
||||||
android:exported="false" />
|
|
||||||
<activity
|
|
||||||
android:name=".ui.RoutingSettingActivity"
|
|
||||||
android:exported="false" />
|
|
||||||
<activity
|
|
||||||
android:name=".ui.RoutingEditActivity"
|
|
||||||
android:exported="false" />
|
|
||||||
<activity
|
|
||||||
android:name=".ui.SubSettingActivity"
|
|
||||||
android:exported="false" />
|
|
||||||
<activity
|
|
||||||
android:name=".ui.UserAssetActivity"
|
|
||||||
android:exported="false" />
|
|
||||||
<activity
|
|
||||||
android:name=".ui.UserAssetUrlActivity"
|
|
||||||
android:exported="false" />
|
|
||||||
|
|
||||||
<activity
|
<activity android:name=".ui.SubEditActivity" />
|
||||||
android:name=".ui.SubEditActivity"
|
<activity android:name=".ui.ScScannerActivity" />
|
||||||
android:exported="false" />
|
<activity android:name=".ui.ScSwitchActivity" />
|
||||||
<activity
|
|
||||||
android:name=".ui.ScScannerActivity"
|
|
||||||
android:exported="false" />
|
|
||||||
<activity
|
|
||||||
android:name=".ui.ScSwitchActivity"
|
|
||||||
android:excludeFromRecents="true"
|
|
||||||
android:exported="false"
|
|
||||||
android:process=":RunSoLibV2RayDaemon"
|
|
||||||
android:theme="@style/AppTheme.NoActionBar.Translucent" />
|
|
||||||
|
|
||||||
<activity
|
|
||||||
android:name=".ui.UrlSchemeActivity"
|
|
||||||
android:exported="true">
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.SEND" />
|
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
|
||||||
<data android:mimeType="text/plain" />
|
|
||||||
</intent-filter>
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.VIEW" />
|
|
||||||
|
|
||||||
<category android:name="android.intent.category.BROWSABLE" />
|
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
|
||||||
|
|
||||||
<data android:scheme="v2rayng" />
|
|
||||||
<data android:host="install-config" />
|
|
||||||
<data android:host="install-sub" />
|
|
||||||
</intent-filter>
|
|
||||||
</activity>
|
|
||||||
<activity
|
|
||||||
android:name=".ui.AboutActivity"
|
|
||||||
android:exported="false" />
|
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name=".service.V2RayVpnService"
|
android:name=".service.V2RayVpnService"
|
||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
android:foregroundServiceType="specialUse"
|
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:permission="android.permission.BIND_VPN_SERVICE"
|
android:permission="android.permission.BIND_VPN_SERVICE"
|
||||||
android:process=":RunSoLibV2RayDaemon">
|
android:process=":RunSoLibV2RayDaemon">
|
||||||
@@ -162,107 +81,45 @@
|
|||||||
<meta-data
|
<meta-data
|
||||||
android:name="android.net.VpnService.SUPPORTS_ALWAYS_ON"
|
android:name="android.net.VpnService.SUPPORTS_ALWAYS_ON"
|
||||||
android:value="true" />
|
android:value="true" />
|
||||||
<property
|
|
||||||
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
|
|
||||||
android:value="vpn" />
|
|
||||||
</service>
|
</service>
|
||||||
|
|
||||||
<service
|
<!--<receiver android:name=".receiver.WidgetProvider">-->
|
||||||
android:name=".service.V2RayProxyOnlyService"
|
<!--<meta-data-->
|
||||||
android:exported="false"
|
<!--android:name="android.appwidget.provider"-->
|
||||||
android:foregroundServiceType="specialUse"
|
<!--android:resource="@xml/app_widget_provider" />-->
|
||||||
android:label="@string/app_name"
|
|
||||||
android:process=":RunSoLibV2RayDaemon">
|
|
||||||
<property
|
|
||||||
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
|
|
||||||
android:value="proxy" />
|
|
||||||
</service>
|
|
||||||
|
|
||||||
<service
|
<!--<intent-filter>-->
|
||||||
android:name=".service.V2RayTestService"
|
<!--<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />-->
|
||||||
android:exported="false"
|
<!--<action android:name="com.v2ray.ang.action.widget.click" />-->
|
||||||
android:process=":RunSoLibV2RayDaemon" />
|
<!--</intent-filter>-->
|
||||||
|
<!--</receiver>-->
|
||||||
<receiver
|
|
||||||
android:name=".receiver.WidgetProvider"
|
|
||||||
android:exported="true"
|
|
||||||
android:process=":RunSoLibV2RayDaemon">
|
|
||||||
<meta-data
|
|
||||||
android:name="android.appwidget.provider"
|
|
||||||
android:resource="@xml/app_widget_provider" />
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
|
||||||
<action android:name="com.v2ray.ang.action.widget.click" />
|
|
||||||
<action android:name="com.v2ray.ang.action.activity" />
|
|
||||||
</intent-filter>
|
|
||||||
</receiver>
|
|
||||||
<receiver
|
|
||||||
android:name=".receiver.BootReceiver"
|
|
||||||
android:exported="true"
|
|
||||||
android:label="BootReceiver">
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
|
||||||
</intent-filter>
|
|
||||||
</receiver>
|
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name=".service.QSTileService"
|
android:name=".service.QSTileService"
|
||||||
android:exported="true"
|
android:icon="@drawable/ic_v"
|
||||||
android:foregroundServiceType="specialUse"
|
|
||||||
android:icon="@drawable/ic_stat_name"
|
|
||||||
android:label="@string/app_tile_name"
|
android:label="@string/app_tile_name"
|
||||||
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE"
|
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
|
||||||
android:process=":RunSoLibV2RayDaemon">
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.service.quicksettings.action.QS_TILE" />
|
<action android:name="android.service.quicksettings.action.QS_TILE" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
<property
|
|
||||||
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
|
|
||||||
android:value="tile" />
|
|
||||||
</service>
|
</service>
|
||||||
<!-- =====================Tasker===================== -->
|
<!-- =====================Tasker===================== -->
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.TaskerActivity"
|
android:name=".ui.TaskerActivity"
|
||||||
android:exported="true"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:icon="@mipmap/ic_launcher">
|
android:label="@string/app_name">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="com.twofortyfouram.locale.intent.action.EDIT_SETTING" />
|
<action android:name="com.twofortyfouram.locale.intent.action.EDIT_SETTING" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<receiver
|
<receiver android:name=".receiver.TaskerReceiver">
|
||||||
android:name=".receiver.TaskerReceiver"
|
|
||||||
android:exported="true"
|
|
||||||
android:process=":RunSoLibV2RayDaemon"
|
|
||||||
tools:ignore="ExportedReceiver">
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="com.twofortyfouram.locale.intent.action.FIRE_SETTING" />
|
<action android:name="com.twofortyfouram.locale.intent.action.FIRE_SETTING" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</receiver>
|
</receiver>
|
||||||
<!-- =====================Tasker===================== -->
|
<!-- =====================Tasker===================== -->
|
||||||
<provider
|
|
||||||
android:name="androidx.startup.InitializationProvider"
|
|
||||||
android:authorities="${applicationId}.androidx-startup"
|
|
||||||
android:exported="false"
|
|
||||||
tools:node="merge">
|
|
||||||
|
|
||||||
<meta-data
|
|
||||||
android:name="androidx.work.WorkManagerInitializer"
|
|
||||||
android:value="androidx.startup"
|
|
||||||
tools:node="remove" />
|
|
||||||
|
|
||||||
</provider>
|
|
||||||
|
|
||||||
<provider
|
|
||||||
android:name="androidx.core.content.FileProvider"
|
|
||||||
android:authorities="${applicationId}.cache"
|
|
||||||
android:exported="false"
|
|
||||||
android:grantUriPermissions="true">
|
|
||||||
<meta-data
|
|
||||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
|
||||||
android:resource="@xml/cache_paths" />
|
|
||||||
</provider>
|
|
||||||
|
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
[
|
|
||||||
{
|
|
||||||
"remarks": "绕过bittorrent",
|
|
||||||
"outboundTag": "direct",
|
|
||||||
"protocol": [
|
|
||||||
"bittorrent"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"remarks": "Google cn",
|
|
||||||
"outboundTag": "proxy",
|
|
||||||
"domain": [
|
|
||||||
"domain:googleapis.cn",
|
|
||||||
"domain:gstatic.com"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"remarks": "阻断udp443",
|
|
||||||
"outboundTag": "block",
|
|
||||||
"port": "443",
|
|
||||||
"network": "udp"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"remarks": "阻断广告",
|
|
||||||
"outboundTag": "block",
|
|
||||||
"domain": [
|
|
||||||
"geosite:category-ads-all"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"remarks": "绕过局域网IP",
|
|
||||||
"outboundTag": "direct",
|
|
||||||
"ip": [
|
|
||||||
"geoip:private"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"remarks": "绕过局域网域名",
|
|
||||||
"outboundTag": "direct",
|
|
||||||
"domain": [
|
|
||||||
"geosite:private"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"remarks": "代理IP",
|
|
||||||
"outboundTag": "proxy",
|
|
||||||
"ip": [
|
|
||||||
"1.0.0.1",
|
|
||||||
"1.1.1.1",
|
|
||||||
"8.8.8.8",
|
|
||||||
"8.8.4.4",
|
|
||||||
"geoip:facebook",
|
|
||||||
"geoip:fastly",
|
|
||||||
"geoip:google",
|
|
||||||
"geoip:netflix",
|
|
||||||
"geoip:telegram",
|
|
||||||
"geoip:twitter"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"remarks": "代理GFW",
|
|
||||||
"outboundTag": "proxy",
|
|
||||||
"domain": [
|
|
||||||
"geosite:gfw",
|
|
||||||
"geosite:greatfire"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"remarks": "最终直连",
|
|
||||||
"port": "0-65535",
|
|
||||||
"outboundTag": "direct"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
[
|
|
||||||
{
|
|
||||||
"remarks": "阻断udp443",
|
|
||||||
"outboundTag": "block",
|
|
||||||
"port": "443",
|
|
||||||
"network": "udp"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"remarks": "阻断广告",
|
|
||||||
"outboundTag": "block",
|
|
||||||
"domain": [
|
|
||||||
"geosite:category-ads-all"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"remarks": "绕过局域网IP",
|
|
||||||
"outboundTag": "direct",
|
|
||||||
"ip": [
|
|
||||||
"geoip:private"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"remarks": "绕过局域网域名",
|
|
||||||
"outboundTag": "direct",
|
|
||||||
"domain": [
|
|
||||||
"geosite:private"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"remarks": "最终代理",
|
|
||||||
"port": "0-65535",
|
|
||||||
"outboundTag": "proxy"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
[
|
|
||||||
{
|
|
||||||
"remarks": "Google cn",
|
|
||||||
"outboundTag": "proxy",
|
|
||||||
"domain": [
|
|
||||||
"domain:googleapis.cn",
|
|
||||||
"domain:gstatic.com"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"remarks": "阻断udp443",
|
|
||||||
"outboundTag": "block",
|
|
||||||
"port": "443",
|
|
||||||
"network": "udp"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"remarks": "阻断广告",
|
|
||||||
"outboundTag": "block",
|
|
||||||
"domain": [
|
|
||||||
"geosite:category-ads-all"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"remarks": "绕过局域网IP",
|
|
||||||
"outboundTag": "direct",
|
|
||||||
"ip": [
|
|
||||||
"geoip:private"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"remarks": "绕过局域网域名",
|
|
||||||
"outboundTag": "direct",
|
|
||||||
"domain": [
|
|
||||||
"geosite:private"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"remarks": "绕过中国IP",
|
|
||||||
"outboundTag": "direct",
|
|
||||||
"ip": [
|
|
||||||
"223.5.5.5/32",
|
|
||||||
"223.6.6.6/32",
|
|
||||||
"2400:3200::1/128",
|
|
||||||
"2400:3200:baba::1/128",
|
|
||||||
"119.29.29.29/32",
|
|
||||||
"1.12.12.12/32",
|
|
||||||
"120.53.53.53/32",
|
|
||||||
"2402:4e00::/128",
|
|
||||||
"2402:4e00:1::/128",
|
|
||||||
"180.76.76.76/32",
|
|
||||||
"2400:da00::6666/128",
|
|
||||||
"114.114.114.114/32",
|
|
||||||
"114.114.115.115/32",
|
|
||||||
"180.184.1.1/32",
|
|
||||||
"180.184.2.2/32",
|
|
||||||
"101.226.4.6/32",
|
|
||||||
"218.30.118.6/32",
|
|
||||||
"123.125.81.6/32",
|
|
||||||
"140.207.198.6/32",
|
|
||||||
"geoip:cn"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"remarks": "绕过中国域名",
|
|
||||||
"outboundTag": "direct",
|
|
||||||
"domain": [
|
|
||||||
"domain:dns.alidns.com",
|
|
||||||
"domain:doh.pub",
|
|
||||||
"domain:dot.pub",
|
|
||||||
"domain:doh.360.cn",
|
|
||||||
"domain:dot.360.cn",
|
|
||||||
"geosite:cn",
|
|
||||||
"geosite:geolocation-cn"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"remarks": "最终代理",
|
|
||||||
"port": "0-65535",
|
|
||||||
"outboundTag": "proxy"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
[
|
|
||||||
{
|
|
||||||
"remarks": "Block udp443",
|
|
||||||
"outboundTag": "block",
|
|
||||||
"port": "443",
|
|
||||||
"network": "udp"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"remarks": "Block ads and trackers",
|
|
||||||
"outboundTag": "block",
|
|
||||||
"domain": [
|
|
||||||
"geosite:category-ads-all"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"remarks": "Direct LAN IP",
|
|
||||||
"outboundTag": "direct",
|
|
||||||
"ip": [
|
|
||||||
"geoip:private"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"remarks": "Direct LAN domains",
|
|
||||||
"outboundTag": "direct",
|
|
||||||
"domain": [
|
|
||||||
"geosite:private"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"remarks": "Bypass Iran domains",
|
|
||||||
"outboundTag": "direct",
|
|
||||||
"domain": [
|
|
||||||
"domain:ir",
|
|
||||||
"geosite:category-ir"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"remarks": "Bypass Iran IP",
|
|
||||||
"outboundTag": "direct",
|
|
||||||
"ip": [
|
|
||||||
"geoip:ir"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"remarks": "Final Agent",
|
|
||||||
"port": "0-65535",
|
|
||||||
"outboundTag": "proxy"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
@@ -1,241 +1,196 @@
|
|||||||
amanita_design.samorost3.gp
|
|
||||||
android
|
|
||||||
au.com.shiftyjelly.pocketcasts
|
|
||||||
bbc.mobile.news.ww
|
|
||||||
be.mygod.vpnhotspot
|
|
||||||
ch.protonmail.android
|
|
||||||
co.wanqu.android
|
|
||||||
com.alphainventor.filemanager
|
|
||||||
com.amazon.kindle
|
|
||||||
com.amazon.mshop.android.shopping
|
|
||||||
com.android.chrome
|
com.android.chrome
|
||||||
com.android.providers.downloads
|
|
||||||
com.android.providers.downloads.ui
|
|
||||||
com.android.providers.telephony
|
|
||||||
com.android.settings
|
|
||||||
com.android.vending
|
|
||||||
com.android6park.m6park
|
|
||||||
com.apkpure.aegon
|
|
||||||
com.apkupdater
|
|
||||||
com.app.pornhub
|
|
||||||
com.arthurivanets.owly
|
|
||||||
com.asahi.tida.tablet
|
|
||||||
com.authy.authy
|
|
||||||
com.avmovie
|
|
||||||
com.ballistiq.artstation
|
|
||||||
com.binance.dev
|
|
||||||
com.bitly.app
|
|
||||||
com.brave.browser
|
|
||||||
com.brave.browser_beta
|
|
||||||
com.breel.wallpapers18
|
|
||||||
com.bvanced.android.youtube
|
|
||||||
com.chrome.beta
|
|
||||||
com.chrome.canary
|
|
||||||
com.chrome.dev
|
|
||||||
com.cl.newt66y
|
|
||||||
com.cradle.iitc_mobile
|
|
||||||
com.cygames.shadowverse
|
|
||||||
com.devhd.feedly
|
|
||||||
com.devolver.reigns2
|
|
||||||
com.discord
|
|
||||||
com.downloader.video.tumblr
|
|
||||||
com.driverbrowser
|
|
||||||
com.dropbox.android
|
|
||||||
com.duolingo
|
|
||||||
com.duckduckgo.mobile.android
|
|
||||||
com.dv.adm
|
|
||||||
com.estrongs.android.pop
|
|
||||||
com.estrongs.android.pop.pro
|
|
||||||
com.evernote
|
|
||||||
com.facebook.katana
|
|
||||||
com.facebook.lite
|
|
||||||
com.facebook.mlite
|
|
||||||
com.facebook.orca
|
|
||||||
com.facebook.services
|
|
||||||
com.facebook.system
|
|
||||||
com.fastaccess.github
|
|
||||||
com.felixfilip.scpae
|
|
||||||
com.fireproofstudios.theroom4
|
|
||||||
com.firstrowria.pushnotificationtester
|
|
||||||
com.flyersoft.moonreaderp
|
|
||||||
com.fooview.android.fooview
|
|
||||||
com.fvd.eversync
|
|
||||||
com.gameloft.android.anmp.glofta8hm
|
|
||||||
com.gameloft.android.anmp.glofta9hm
|
|
||||||
com.gianlu.aria2app
|
|
||||||
com.github.yeriomin.yalpstore
|
|
||||||
com.google.android.apps.adm
|
|
||||||
com.google.android.apps.books
|
|
||||||
com.google.android.apps.docs
|
|
||||||
com.google.android.apps.docs.editors.sheets
|
|
||||||
com.google.android.apps.fitness
|
|
||||||
com.google.android.apps.googleassistant
|
|
||||||
com.google.android.apps.googlevoice
|
|
||||||
com.google.android.apps.hangoutsdialer
|
|
||||||
com.google.android.apps.inbox
|
|
||||||
com.google.android.apps.magazines
|
|
||||||
com.google.android.apps.maps
|
|
||||||
com.google.android.apps.nbu.files
|
|
||||||
com.google.android.apps.paidtasks
|
|
||||||
com.google.android.apps.pdfviewer
|
|
||||||
com.google.android.apps.photos
|
|
||||||
com.google.android.apps.plus
|
|
||||||
com.google.android.apps.translate
|
|
||||||
com.google.android.gm
|
|
||||||
com.google.android.gms
|
|
||||||
com.google.android.gms.setup
|
|
||||||
com.google.android.googlequicksearchbox
|
com.google.android.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
|
||||||
com.google.android.gsf.login
|
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.google.android.ims
|
||||||
com.google.android.inputmethod.latin
|
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.instantapps.supervisor
|
||||||
com.google.android.keep
|
|
||||||
com.google.android.music
|
|
||||||
com.google.android.ogyoutube
|
com.google.android.ogyoutube
|
||||||
com.google.android.partnersetup
|
com.google.android.partnersetup
|
||||||
com.google.android.play.games
|
|
||||||
com.google.android.street
|
|
||||||
com.google.android.syncadapters.calendar
|
com.google.android.syncadapters.calendar
|
||||||
com.google.android.syncadapters.contacts
|
com.google.android.syncadapters.contacts
|
||||||
com.google.android.talk
|
|
||||||
com.google.android.tts
|
com.google.android.tts
|
||||||
com.google.android.videos
|
|
||||||
com.google.android.youtube
|
|
||||||
com.google.ar.lens
|
|
||||||
com.hochan.coldsoup
|
com.hochan.coldsoup
|
||||||
com.ifttt.ifttt
|
com.ifttt.ifttt
|
||||||
com.imgur.mobile
|
com.imgur.mobile
|
||||||
com.innologica.inoreader
|
com.innologica.inoreader
|
||||||
com.instagram.android
|
|
||||||
com.instapaper.android
|
com.instapaper.android
|
||||||
com.jarvanh.vpntether
|
com.jarvanh.vpntether
|
||||||
com.kapp.youtube.final
|
|
||||||
com.klinker.android.twitter_l
|
|
||||||
com.lastpass.lpandroid
|
|
||||||
com.linecorp.linelite
|
|
||||||
com.lingodeer
|
|
||||||
com.mediapods.tumbpods
|
com.mediapods.tumbpods
|
||||||
com.mgoogle.android.gms
|
com.mgoogle.android.gms
|
||||||
com.microsoft.emmx
|
|
||||||
com.microsoft.office.powerpoint
|
com.microsoft.office.powerpoint
|
||||||
com.microsoft.skydrive
|
|
||||||
com.mixplorer
|
com.mixplorer
|
||||||
com.msd.consumerchinese
|
com.msd.consumerchinese
|
||||||
com.msd.professionalchinese
|
com.msd.professionalchinese
|
||||||
com.mss2011c.sharehelper
|
com.mss2011c.sharehelper
|
||||||
com.netflix.mediaclient
|
|
||||||
com.newin.nplayer.pro
|
com.newin.nplayer.pro
|
||||||
com.nianticlabs.ingress.prime.qa
|
|
||||||
com.nianticproject.ingress
|
|
||||||
com.ninefolders.hd3
|
|
||||||
com.ninegag.android.app
|
|
||||||
com.nintendo.zara
|
|
||||||
com.nytimes.cn
|
|
||||||
com.oasisfeng.island
|
com.oasisfeng.island
|
||||||
com.ocnt.liveapp.hw
|
|
||||||
com.orekie.search
|
com.orekie.search
|
||||||
com.patreon.android
|
|
||||||
com.paypal.android.p2pmobile
|
|
||||||
com.perol.asdpl.pixivez
|
|
||||||
com.pinterest
|
|
||||||
com.popularapp.periodcalendar
|
|
||||||
com.popularapp.videodownloaderforinstagram
|
com.popularapp.videodownloaderforinstagram
|
||||||
com.pushbullet.android
|
com.pushbullet.android
|
||||||
com.quoord.tapatalkpro.activity
|
|
||||||
com.quora.android
|
|
||||||
com.rayark.cytus2
|
|
||||||
com.rayark.implosion
|
|
||||||
com.rayark.pluto
|
|
||||||
com.reddit.frontpage
|
|
||||||
com.resilio.sync
|
|
||||||
com.rhmsoft.edit
|
com.rhmsoft.edit
|
||||||
com.rubenmayayo.reddit
|
|
||||||
com.sec.android.app.sbrowser
|
|
||||||
com.sec.android.app.sbrowser.beta
|
|
||||||
com.shanga.walli
|
|
||||||
com.simplehabit.simplehabitapp
|
|
||||||
com.slack
|
com.slack
|
||||||
com.snaptube.premium
|
|
||||||
com.sololearn
|
|
||||||
com.sonelli.juicessh
|
|
||||||
com.spotify.music
|
|
||||||
com.tencent.huatuo
|
com.tencent.huatuo
|
||||||
com.termux
|
com.termux
|
||||||
com.teslacoilsw.launcher
|
|
||||||
com.theinitium.news
|
|
||||||
com.thomsonreuters.reuters
|
|
||||||
com.thunkable.android.hritvik00.freenom
|
com.thunkable.android.hritvik00.freenom
|
||||||
com.topjohnwu.magisk
|
com.topjohnwu.magisk
|
||||||
com.tripadvisor.tripadvisor
|
|
||||||
com.tumblr
|
|
||||||
com.twitter.android
|
|
||||||
com.u91porn
|
com.u91porn
|
||||||
com.u9porn
|
com.u9porn
|
||||||
com.ubisoft.dance.justdance2015companion
|
|
||||||
com.utopia.pxview
|
|
||||||
com.valvesoftware.android.steam.communimunity
|
|
||||||
com.valvesoftware.android.steam.community
|
|
||||||
com.vanced.android.youtube
|
|
||||||
com.vimeo.android.videoapp
|
com.vimeo.android.videoapp
|
||||||
com.vivaldi.browser
|
|
||||||
com.vivaldi.browser.snapshot
|
|
||||||
com.vkontakte.android
|
|
||||||
com.whatsapp
|
|
||||||
com.wire
|
|
||||||
com.wuxiangai.refactor
|
com.wuxiangai.refactor
|
||||||
com.xda.labs
|
|
||||||
com.xvideos.app
|
|
||||||
com.yandex.browser
|
com.yandex.browser
|
||||||
com.yandex.browser.beta
|
|
||||||
com.yandex.browser.alpha
|
|
||||||
com.z28j.feel
|
com.z28j.feel
|
||||||
con.medium.reader
|
|
||||||
de.apkgrabber
|
|
||||||
de.robv.android.xposed.installer
|
de.robv.android.xposed.installer
|
||||||
dk.tacit.android.foldersync.full
|
dk.tacit.android.foldersync.full
|
||||||
es.rafalense.telegram.themes
|
es.rafalense.telegram.themes
|
||||||
es.rafalense.themes
|
es.rafalense.themes
|
||||||
flipboard.app
|
flipboard.app
|
||||||
fm.moon.app
|
|
||||||
fr.gouv.etalab.mastodon
|
|
||||||
github.tornaco.xposedmoduletest
|
github.tornaco.xposedmoduletest
|
||||||
idm.internet.download.manager
|
|
||||||
idm.internet.download.manager.plus
|
|
||||||
io.github.javiewer
|
|
||||||
io.github.skyhacker2.magnetsearch
|
|
||||||
io.va.exposed
|
io.va.exposed
|
||||||
it.mvilla.android.fenix2
|
|
||||||
jp.bokete.app.android
|
|
||||||
jp.naver.line.android
|
|
||||||
jp.pxv.android
|
jp.pxv.android
|
||||||
luo.speedometergpspro
|
|
||||||
mark.via.gp
|
|
||||||
me.tshine.easymark
|
me.tshine.easymark
|
||||||
net.teeha.android.url_shortener
|
net.teeha.android.url_shortener
|
||||||
net.tsapps.appsales
|
|
||||||
onion.fire
|
onion.fire
|
||||||
org.fdroid.fdroid
|
org.fdroid.fdroid
|
||||||
org.freedownloadmanager.fdm
|
|
||||||
org.kustom.widget
|
|
||||||
org.mozilla.fennec_aurora
|
org.mozilla.fennec_aurora
|
||||||
org.mozilla.fenix
|
|
||||||
org.mozilla.fenix.nightly
|
|
||||||
org.mozilla.firefox
|
|
||||||
org.mozilla.firefox_beta
|
|
||||||
org.mozilla.focus
|
|
||||||
org.schabi.newpipe
|
org.schabi.newpipe
|
||||||
org.telegram.messenger
|
org.telegram.messenger
|
||||||
org.telegram.multi
|
|
||||||
org.telegram.plus
|
|
||||||
org.thunderdog.challegram
|
|
||||||
org.torproject.android
|
org.torproject.android
|
||||||
org.torproject.torbrowser_alpha
|
|
||||||
org.wikipedia
|
|
||||||
org.xbmc.kodi
|
org.xbmc.kodi
|
||||||
pl.zdunex25.updater
|
pl.zdunex25.updater
|
||||||
tv.twitch.android.app
|
|
||||||
tw.com.gamer.android.activecenter
|
|
||||||
videodownloader.downloadvideo.downloader
|
videodownloader.downloadvideo.downloader
|
||||||
uk.co.bbc.learningenglish
|
com.quora.android
|
||||||
com.ted.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
|
||||||
@@ -13,8 +13,8 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"system": {
|
"system": {
|
||||||
"statsOutboundUplink": true,
|
"statsInboundUplink": true,
|
||||||
"statsOutboundDownlink": true
|
"statsInboundDownlink": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"inbounds": [{
|
"inbounds": [{
|
||||||
@@ -54,7 +54,7 @@
|
|||||||
"users": [
|
"users": [
|
||||||
{
|
{
|
||||||
"id": "a3482e88-686a-4a58-8126-99c9df64b7bf",
|
"id": "a3482e88-686a-4a58-8126-99c9df64b7bf",
|
||||||
"alterId": 0,
|
"alterId": 64,
|
||||||
"security": "auto",
|
"security": "auto",
|
||||||
"level": 8
|
"level": 8
|
||||||
}
|
}
|
||||||
@@ -81,9 +81,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"protocol": "freedom",
|
"protocol": "freedom",
|
||||||
"settings": {
|
"settings": {},
|
||||||
"domainStrategy": "UseIP"
|
|
||||||
},
|
|
||||||
"tag": "direct"
|
"tag": "direct"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 28 KiB |
@@ -16,8 +16,8 @@
|
|||||||
|
|
||||||
package com.v2ray.ang.helper;
|
package com.v2ray.ang.helper;
|
||||||
|
|
||||||
import androidx.recyclerview.widget.ItemTouchHelper;
|
import android.support.v7.widget.RecyclerView;
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
import android.support.v7.widget.helper.ItemTouchHelper;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Interface to listen for a move or dismissal event from a {@link ItemTouchHelper.Callback}.
|
* Interface to listen for a move or dismissal event from a {@link ItemTouchHelper.Callback}.
|
||||||
@@ -36,14 +36,13 @@ public interface ItemTouchHelperAdapter {
|
|||||||
* @param fromPosition The start position of the moved item.
|
* @param fromPosition The start position of the moved item.
|
||||||
* @param toPosition Then resolved 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.
|
* @return True if the item was moved to the new adapter position.
|
||||||
|
*
|
||||||
* @see RecyclerView#getAdapterPositionFor(RecyclerView.ViewHolder)
|
* @see RecyclerView#getAdapterPositionFor(RecyclerView.ViewHolder)
|
||||||
* @see RecyclerView.ViewHolder#getAdapterPosition()
|
* @see RecyclerView.ViewHolder#getAdapterPosition()
|
||||||
*/
|
*/
|
||||||
boolean onItemMove(int fromPosition, int toPosition);
|
boolean onItemMove(int fromPosition, int toPosition);
|
||||||
|
|
||||||
|
|
||||||
void onItemMoveCompleted();
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when an item has been dismissed by a swipe.<br/>
|
* Called when an item has been dismissed by a swipe.<br/>
|
||||||
* <br/>
|
* <br/>
|
||||||
@@ -51,6 +50,7 @@ public interface ItemTouchHelperAdapter {
|
|||||||
* adjusting the underlying data to reflect this removal.
|
* adjusting the underlying data to reflect this removal.
|
||||||
*
|
*
|
||||||
* @param position The position of the item dismissed.
|
* @param position The position of the item dismissed.
|
||||||
|
*
|
||||||
* @see RecyclerView#getAdapterPositionFor(RecyclerView.ViewHolder)
|
* @see RecyclerView#getAdapterPositionFor(RecyclerView.ViewHolder)
|
||||||
* @see RecyclerView.ViewHolder#getAdapterPosition()
|
* @see RecyclerView.ViewHolder#getAdapterPosition()
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
|
|
||||||
package com.v2ray.ang.helper;
|
package com.v2ray.ang.helper;
|
||||||
|
|
||||||
import androidx.recyclerview.widget.ItemTouchHelper;
|
import android.support.v7.widget.helper.ItemTouchHelper;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Interface to notify an item ViewHolder of relevant callbacks from {@link
|
* Interface to notify an item ViewHolder of relevant callbacks from {@link
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
}
|
||||||
@@ -16,14 +16,10 @@
|
|||||||
|
|
||||||
package com.v2ray.ang.helper;
|
package com.v2ray.ang.helper;
|
||||||
|
|
||||||
import android.animation.ValueAnimator;
|
|
||||||
import android.graphics.Canvas;
|
import android.graphics.Canvas;
|
||||||
import android.view.animation.DecelerateInterpolator;
|
import android.support.v7.widget.GridLayoutManager;
|
||||||
|
import android.support.v7.widget.RecyclerView;
|
||||||
import androidx.annotation.NonNull;
|
import android.support.v7.widget.helper.ItemTouchHelper;
|
||||||
import androidx.recyclerview.widget.GridLayoutManager;
|
|
||||||
import androidx.recyclerview.widget.ItemTouchHelper;
|
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An implementation of {@link ItemTouchHelper.Callback} that enables basic drag & drop and
|
* An implementation of {@link ItemTouchHelper.Callback} that enables basic drag & drop and
|
||||||
@@ -37,12 +33,9 @@ import androidx.recyclerview.widget.RecyclerView;
|
|||||||
*/
|
*/
|
||||||
public class SimpleItemTouchHelperCallback extends ItemTouchHelper.Callback {
|
public class SimpleItemTouchHelperCallback extends ItemTouchHelper.Callback {
|
||||||
|
|
||||||
private static final float ALPHA_FULL = 1.0f;
|
public static final float ALPHA_FULL = 1.0f;
|
||||||
private static final float SWIPE_THRESHOLD = 0.25f;
|
|
||||||
private static final long ANIMATION_DURATION = 200;
|
|
||||||
|
|
||||||
private final ItemTouchHelperAdapter mAdapter;
|
private final ItemTouchHelperAdapter mAdapter;
|
||||||
private ValueAnimator mReturnAnimator;
|
|
||||||
|
|
||||||
public SimpleItemTouchHelperCallback(ItemTouchHelperAdapter adapter) {
|
public SimpleItemTouchHelperCallback(ItemTouchHelperAdapter adapter) {
|
||||||
mAdapter = adapter;
|
mAdapter = adapter;
|
||||||
@@ -55,14 +48,15 @@ public class SimpleItemTouchHelperCallback extends ItemTouchHelper.Callback {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isItemViewSwipeEnabled() {
|
public boolean isItemViewSwipeEnabled() {
|
||||||
return true;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getMovementFlags(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) {
|
public int getMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
|
||||||
|
// Set movement flags based on the layout manager
|
||||||
if (recyclerView.getLayoutManager() instanceof GridLayoutManager) {
|
if (recyclerView.getLayoutManager() instanceof GridLayoutManager) {
|
||||||
final int dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN | ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT;
|
final int dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN | ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT;
|
||||||
final int swipeFlags = ItemTouchHelper.START | ItemTouchHelper.END;
|
final int swipeFlags = 0;
|
||||||
return makeMovementFlags(dragFlags, swipeFlags);
|
return makeMovementFlags(dragFlags, swipeFlags);
|
||||||
} else {
|
} else {
|
||||||
final int dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN;
|
final int dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN;
|
||||||
@@ -72,89 +66,58 @@ public class SimpleItemTouchHelperCallback extends ItemTouchHelper.Callback {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onMove(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder source, @NonNull RecyclerView.ViewHolder target) {
|
public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder source, RecyclerView.ViewHolder target) {
|
||||||
if (source.getItemViewType() != target.getItemViewType()) {
|
if (source.getItemViewType() != target.getItemViewType()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
mAdapter.onItemMove(source.getBindingAdapterPosition(), target.getBindingAdapterPosition());
|
|
||||||
|
// Notify the adapter of the move
|
||||||
|
mAdapter.onItemMove(source.getAdapterPosition(), target.getAdapterPosition());
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) {
|
public void onSwiped(RecyclerView.ViewHolder viewHolder, int i) {
|
||||||
// 不执行删除操作,仅返回项目到原位
|
// Notify the adapter of the dismissal
|
||||||
returnViewToOriginalPosition(viewHolder);
|
mAdapter.onItemDismiss(viewHolder.getAdapterPosition());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onChildDraw(@NonNull Canvas c, @NonNull RecyclerView recyclerView,
|
public void onChildDraw(Canvas c, RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, boolean isCurrentlyActive) {
|
||||||
@NonNull RecyclerView.ViewHolder viewHolder,
|
|
||||||
float dX, float dY, int actionState, boolean isCurrentlyActive) {
|
|
||||||
if (actionState == ItemTouchHelper.ACTION_STATE_SWIPE) {
|
if (actionState == ItemTouchHelper.ACTION_STATE_SWIPE) {
|
||||||
float maxSwipeDistance = viewHolder.itemView.getWidth() * SWIPE_THRESHOLD;
|
// Fade out the view as it is swiped out of the parent's bounds
|
||||||
float swipeAmount = Math.abs(dX);
|
final float alpha = ALPHA_FULL - Math.abs(dX) / (float) viewHolder.itemView.getWidth();
|
||||||
float direction = Math.signum(dX);
|
|
||||||
|
|
||||||
// 限制最大滑动距离
|
|
||||||
float translationX = Math.min(swipeAmount, maxSwipeDistance) * direction;
|
|
||||||
float alpha = ALPHA_FULL - Math.min(swipeAmount, maxSwipeDistance) / maxSwipeDistance;
|
|
||||||
|
|
||||||
viewHolder.itemView.setTranslationX(translationX);
|
|
||||||
viewHolder.itemView.setAlpha(alpha);
|
viewHolder.itemView.setAlpha(alpha);
|
||||||
|
viewHolder.itemView.setTranslationX(dX);
|
||||||
if (swipeAmount >= maxSwipeDistance && isCurrentlyActive) {
|
|
||||||
returnViewToOriginalPosition(viewHolder);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive);
|
super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void returnViewToOriginalPosition(RecyclerView.ViewHolder viewHolder) {
|
|
||||||
if (mReturnAnimator != null && mReturnAnimator.isRunning()) {
|
|
||||||
mReturnAnimator.cancel();
|
|
||||||
}
|
|
||||||
|
|
||||||
mReturnAnimator = ValueAnimator.ofFloat(viewHolder.itemView.getTranslationX(), 0f);
|
|
||||||
mReturnAnimator.addUpdateListener(animation -> {
|
|
||||||
float value = (float) animation.getAnimatedValue();
|
|
||||||
viewHolder.itemView.setTranslationX(value);
|
|
||||||
viewHolder.itemView.setAlpha(1f - Math.abs(value) / (viewHolder.itemView.getWidth() * SWIPE_THRESHOLD));
|
|
||||||
});
|
|
||||||
mReturnAnimator.setInterpolator(new DecelerateInterpolator());
|
|
||||||
mReturnAnimator.setDuration(ANIMATION_DURATION);
|
|
||||||
mReturnAnimator.start();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onSelectedChanged(RecyclerView.ViewHolder viewHolder, int actionState) {
|
public void onSelectedChanged(RecyclerView.ViewHolder viewHolder, int actionState) {
|
||||||
|
// We only want the active item to change
|
||||||
if (actionState != ItemTouchHelper.ACTION_STATE_IDLE) {
|
if (actionState != ItemTouchHelper.ACTION_STATE_IDLE) {
|
||||||
if (viewHolder instanceof ItemTouchHelperViewHolder) {
|
if (viewHolder instanceof ItemTouchHelperViewHolder) {
|
||||||
|
// Let the view holder know that this item is being moved or dragged
|
||||||
ItemTouchHelperViewHolder itemViewHolder = (ItemTouchHelperViewHolder) viewHolder;
|
ItemTouchHelperViewHolder itemViewHolder = (ItemTouchHelperViewHolder) viewHolder;
|
||||||
itemViewHolder.onItemSelected();
|
itemViewHolder.onItemSelected();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
super.onSelectedChanged(viewHolder, actionState);
|
super.onSelectedChanged(viewHolder, actionState);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void clearView(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) {
|
public void clearView(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
|
||||||
super.clearView(recyclerView, viewHolder);
|
super.clearView(recyclerView, viewHolder);
|
||||||
|
|
||||||
viewHolder.itemView.setAlpha(ALPHA_FULL);
|
viewHolder.itemView.setAlpha(ALPHA_FULL);
|
||||||
|
|
||||||
if (viewHolder instanceof ItemTouchHelperViewHolder) {
|
if (viewHolder instanceof ItemTouchHelperViewHolder) {
|
||||||
|
// Tell the view holder it's time to restore the idle state
|
||||||
ItemTouchHelperViewHolder itemViewHolder = (ItemTouchHelperViewHolder) viewHolder;
|
ItemTouchHelperViewHolder itemViewHolder = (ItemTouchHelperViewHolder) viewHolder;
|
||||||
itemViewHolder.onItemClear();
|
itemViewHolder.onItemClear();
|
||||||
}
|
}
|
||||||
mAdapter.onItemMoveCompleted();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public float getSwipeThreshold(@NonNull RecyclerView.ViewHolder viewHolder) {
|
|
||||||
return 1.1f; // 设置一个大于1的值,确保不会触发默认的滑动删除操作
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public float getSwipeEscapeVelocity(float defaultValue) {
|
|
||||||
return defaultValue * 10; // 增加滑动逃逸速度,使得更难触发滑动
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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:
|
||||||
|
* <p>
|
||||||
|
* I am placing this code in the Public Domain. Do with it as you will.
|
||||||
|
* This software comes with no guarantees or warranties but with
|
||||||
|
* plenty of well-wishing instead!
|
||||||
|
* Please visit
|
||||||
|
* <a href="http://iharder.net/xmlizable">http://iharder.net/xmlizable</a>
|
||||||
|
* periodically to check for updates or to contribute improvements.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @author Robert Harder
|
||||||
|
* @author rharder@usa.net
|
||||||
|
* @version 1.3
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base64 converter class. This code is not a complete MIME encoder;
|
||||||
|
* it simply converts binary data to base64 data and back.
|
||||||
|
*
|
||||||
|
* <p>Note {@link CharBase64} is a GWT-compatible implementation of this
|
||||||
|
* class.
|
||||||
|
*/
|
||||||
|
public class Base64 {
|
||||||
|
/** Specify encoding (value is {@code true}). */
|
||||||
|
public final static boolean ENCODE = true;
|
||||||
|
|
||||||
|
/** Specify decoding (value is {@code false}). */
|
||||||
|
public final static boolean DECODE = false;
|
||||||
|
|
||||||
|
/** The equals sign (=) as a byte. */
|
||||||
|
private final static byte EQUALS_SIGN = (byte) '=';
|
||||||
|
|
||||||
|
/** The new line character (\n) as a byte. */
|
||||||
|
private final static byte NEW_LINE = (byte) '\n';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The 64 valid Base64 values.
|
||||||
|
*/
|
||||||
|
private final static byte[] ALPHABET =
|
||||||
|
{(byte) 'A', (byte) 'B', (byte) 'C', (byte) 'D', (byte) 'E', (byte) 'F',
|
||||||
|
(byte) 'G', (byte) 'H', (byte) 'I', (byte) 'J', (byte) 'K',
|
||||||
|
(byte) 'L', (byte) 'M', (byte) 'N', (byte) 'O', (byte) 'P',
|
||||||
|
(byte) 'Q', (byte) 'R', (byte) 'S', (byte) 'T', (byte) 'U',
|
||||||
|
(byte) 'V', (byte) 'W', (byte) 'X', (byte) 'Y', (byte) 'Z',
|
||||||
|
(byte) 'a', (byte) 'b', (byte) 'c', (byte) 'd', (byte) 'e',
|
||||||
|
(byte) 'f', (byte) 'g', (byte) 'h', (byte) 'i', (byte) 'j',
|
||||||
|
(byte) 'k', (byte) 'l', (byte) 'm', (byte) 'n', (byte) 'o',
|
||||||
|
(byte) 'p', (byte) 'q', (byte) 'r', (byte) 's', (byte) 't',
|
||||||
|
(byte) 'u', (byte) 'v', (byte) 'w', (byte) 'x', (byte) 'y',
|
||||||
|
(byte) 'z', (byte) '0', (byte) '1', (byte) '2', (byte) '3',
|
||||||
|
(byte) '4', (byte) '5', (byte) '6', (byte) '7', (byte) '8',
|
||||||
|
(byte) '9', (byte) '+', (byte) '/'};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The 64 valid web safe Base64 values.
|
||||||
|
*/
|
||||||
|
private final static byte[] WEBSAFE_ALPHABET =
|
||||||
|
{(byte) 'A', (byte) 'B', (byte) 'C', (byte) 'D', (byte) 'E', (byte) 'F',
|
||||||
|
(byte) 'G', (byte) 'H', (byte) 'I', (byte) 'J', (byte) 'K',
|
||||||
|
(byte) 'L', (byte) 'M', (byte) 'N', (byte) 'O', (byte) 'P',
|
||||||
|
(byte) 'Q', (byte) 'R', (byte) 'S', (byte) 'T', (byte) 'U',
|
||||||
|
(byte) 'V', (byte) 'W', (byte) 'X', (byte) 'Y', (byte) 'Z',
|
||||||
|
(byte) 'a', (byte) 'b', (byte) 'c', (byte) 'd', (byte) 'e',
|
||||||
|
(byte) 'f', (byte) 'g', (byte) 'h', (byte) 'i', (byte) 'j',
|
||||||
|
(byte) 'k', (byte) 'l', (byte) 'm', (byte) 'n', (byte) 'o',
|
||||||
|
(byte) 'p', (byte) 'q', (byte) 'r', (byte) 's', (byte) 't',
|
||||||
|
(byte) 'u', (byte) 'v', (byte) 'w', (byte) 'x', (byte) 'y',
|
||||||
|
(byte) 'z', (byte) '0', (byte) '1', (byte) '2', (byte) '3',
|
||||||
|
(byte) '4', (byte) '5', (byte) '6', (byte) '7', (byte) '8',
|
||||||
|
(byte) '9', (byte) '-', (byte) '_'};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Translates a Base64 value to either its 6-bit reconstruction value
|
||||||
|
* or a negative number indicating some other meaning.
|
||||||
|
**/
|
||||||
|
private final static byte[] DECODABET = {-9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 0 - 8
|
||||||
|
-5, -5, // Whitespace: Tab and Linefeed
|
||||||
|
-9, -9, // Decimal 11 - 12
|
||||||
|
-5, // Whitespace: Carriage Return
|
||||||
|
-9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 14 - 26
|
||||||
|
-9, -9, -9, -9, -9, // Decimal 27 - 31
|
||||||
|
-5, // Whitespace: Space
|
||||||
|
-9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 33 - 42
|
||||||
|
62, // Plus sign at decimal 43
|
||||||
|
-9, -9, -9, // Decimal 44 - 46
|
||||||
|
63, // Slash at decimal 47
|
||||||
|
52, 53, 54, 55, 56, 57, 58, 59, 60, 61, // Numbers zero through nine
|
||||||
|
-9, -9, -9, // Decimal 58 - 60
|
||||||
|
-1, // Equals sign at decimal 61
|
||||||
|
-9, -9, -9, // Decimal 62 - 64
|
||||||
|
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, // Letters 'A' through 'N'
|
||||||
|
14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, // Letters 'O' through 'Z'
|
||||||
|
-9, -9, -9, -9, -9, -9, // Decimal 91 - 96
|
||||||
|
26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, // Letters 'a' through 'm'
|
||||||
|
39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, // Letters 'n' through 'z'
|
||||||
|
-9, -9, -9, -9, -9 // Decimal 123 - 127
|
||||||
|
/* ,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 128 - 139
|
||||||
|
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 140 - 152
|
||||||
|
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 153 - 165
|
||||||
|
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 166 - 178
|
||||||
|
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 179 - 191
|
||||||
|
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 192 - 204
|
||||||
|
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 205 - 217
|
||||||
|
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 218 - 230
|
||||||
|
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 231 - 243
|
||||||
|
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9 // Decimal 244 - 255 */
|
||||||
|
};
|
||||||
|
|
||||||
|
/** The web safe decodabet */
|
||||||
|
private final static byte[] WEBSAFE_DECODABET =
|
||||||
|
{-9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 0 - 8
|
||||||
|
-5, -5, // Whitespace: Tab and Linefeed
|
||||||
|
-9, -9, // Decimal 11 - 12
|
||||||
|
-5, // Whitespace: Carriage Return
|
||||||
|
-9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 14 - 26
|
||||||
|
-9, -9, -9, -9, -9, // Decimal 27 - 31
|
||||||
|
-5, // Whitespace: Space
|
||||||
|
-9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 33 - 44
|
||||||
|
62, // Dash '-' sign at decimal 45
|
||||||
|
-9, -9, // Decimal 46-47
|
||||||
|
52, 53, 54, 55, 56, 57, 58, 59, 60, 61, // Numbers zero through nine
|
||||||
|
-9, -9, -9, // Decimal 58 - 60
|
||||||
|
-1, // Equals sign at decimal 61
|
||||||
|
-9, -9, -9, // Decimal 62 - 64
|
||||||
|
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, // Letters 'A' through 'N'
|
||||||
|
14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, // Letters 'O' through 'Z'
|
||||||
|
-9, -9, -9, -9, // Decimal 91-94
|
||||||
|
63, // Underscore '_' at decimal 95
|
||||||
|
-9, // Decimal 96
|
||||||
|
26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, // Letters 'a' through 'm'
|
||||||
|
39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, // Letters 'n' through 'z'
|
||||||
|
-9, -9, -9, -9, -9 // Decimal 123 - 127
|
||||||
|
/* ,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 128 - 139
|
||||||
|
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 140 - 152
|
||||||
|
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 153 - 165
|
||||||
|
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 166 - 178
|
||||||
|
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 179 - 191
|
||||||
|
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 192 - 204
|
||||||
|
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 205 - 217
|
||||||
|
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 218 - 230
|
||||||
|
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 231 - 243
|
||||||
|
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9 // Decimal 244 - 255 */
|
||||||
|
};
|
||||||
|
|
||||||
|
// Indicates white space in encoding
|
||||||
|
private final static byte WHITE_SPACE_ENC = -5;
|
||||||
|
// Indicates equals sign in encoding
|
||||||
|
private final static byte EQUALS_SIGN_ENC = -1;
|
||||||
|
|
||||||
|
/** Defeats instantiation. */
|
||||||
|
private Base64() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ******** E N C O D I N G M E T H O D S ******** */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encodes up to three bytes of the array <var>source</var>
|
||||||
|
* and writes the resulting four Base64 bytes to <var>destination</var>.
|
||||||
|
* The source and destination arrays can be manipulated
|
||||||
|
* anywhere along their length by specifying
|
||||||
|
* <var>srcOffset</var> and <var>destOffset</var>.
|
||||||
|
* This method does not check to make sure your arrays
|
||||||
|
* are large enough to accommodate <var>srcOffset</var> + 3 for
|
||||||
|
* the <var>source</var> array or <var>destOffset</var> + 4 for
|
||||||
|
* the <var>destination</var> array.
|
||||||
|
* The actual number of significant bytes in your array is
|
||||||
|
* given by <var>numSigBytes</var>.
|
||||||
|
*
|
||||||
|
* @param source the array to convert
|
||||||
|
* @param srcOffset the index where conversion begins
|
||||||
|
* @param numSigBytes the number of significant bytes in your array
|
||||||
|
* @param destination the array to hold the conversion
|
||||||
|
* @param destOffset the index where output will be put
|
||||||
|
* @param alphabet is the encoding alphabet
|
||||||
|
* @return the <var>destination</var> array
|
||||||
|
* @since 1.3
|
||||||
|
*/
|
||||||
|
private static byte[] encode3to4(byte[] source, int srcOffset,
|
||||||
|
int numSigBytes, byte[] destination, int destOffset, byte[] alphabet) {
|
||||||
|
// 1 2 3
|
||||||
|
// 01234567890123456789012345678901 Bit position
|
||||||
|
// --------000000001111111122222222 Array position from threeBytes
|
||||||
|
// --------| || || || | Six bit groups to index alphabet
|
||||||
|
// >>18 >>12 >> 6 >> 0 Right shift necessary
|
||||||
|
// 0x3f 0x3f 0x3f Additional AND
|
||||||
|
|
||||||
|
// Create buffer with zero-padding if there are only one or two
|
||||||
|
// significant bytes passed in the array.
|
||||||
|
// We have to shift left 24 in order to flush out the 1's that appear
|
||||||
|
// when Java treats a value as negative that is cast from a byte to an int.
|
||||||
|
int inBuff =
|
||||||
|
(numSigBytes > 0 ? ((source[srcOffset] << 24) >>> 8) : 0)
|
||||||
|
| (numSigBytes > 1 ? ((source[srcOffset + 1] << 24) >>> 16) : 0)
|
||||||
|
| (numSigBytes > 2 ? ((source[srcOffset + 2] << 24) >>> 24) : 0);
|
||||||
|
|
||||||
|
switch (numSigBytes) {
|
||||||
|
case 3:
|
||||||
|
destination[destOffset] = alphabet[(inBuff >>> 18)];
|
||||||
|
destination[destOffset + 1] = alphabet[(inBuff >>> 12) & 0x3f];
|
||||||
|
destination[destOffset + 2] = alphabet[(inBuff >>> 6) & 0x3f];
|
||||||
|
destination[destOffset + 3] = alphabet[(inBuff) & 0x3f];
|
||||||
|
return destination;
|
||||||
|
case 2:
|
||||||
|
destination[destOffset] = alphabet[(inBuff >>> 18)];
|
||||||
|
destination[destOffset + 1] = alphabet[(inBuff >>> 12) & 0x3f];
|
||||||
|
destination[destOffset + 2] = alphabet[(inBuff >>> 6) & 0x3f];
|
||||||
|
destination[destOffset + 3] = EQUALS_SIGN;
|
||||||
|
return destination;
|
||||||
|
case 1:
|
||||||
|
destination[destOffset] = alphabet[(inBuff >>> 18)];
|
||||||
|
destination[destOffset + 1] = alphabet[(inBuff >>> 12) & 0x3f];
|
||||||
|
destination[destOffset + 2] = EQUALS_SIGN;
|
||||||
|
destination[destOffset + 3] = EQUALS_SIGN;
|
||||||
|
return destination;
|
||||||
|
default:
|
||||||
|
return destination;
|
||||||
|
} // end switch
|
||||||
|
} // end encode3to4
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encodes a byte array into Base64 notation.
|
||||||
|
* Equivalent to calling
|
||||||
|
* {@code encodeBytes(source, 0, source.length)}
|
||||||
|
*
|
||||||
|
* @param source The data to convert
|
||||||
|
* @since 1.4
|
||||||
|
*/
|
||||||
|
public static String encode(byte[] source) {
|
||||||
|
return encode(source, 0, source.length, ALPHABET, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encodes a byte array into web safe Base64 notation.
|
||||||
|
*
|
||||||
|
* @param source The data to convert
|
||||||
|
* @param doPadding is {@code true} to pad result with '=' chars
|
||||||
|
* if it does not fall on 3 byte boundaries
|
||||||
|
*/
|
||||||
|
public static String encodeWebSafe(byte[] source, boolean doPadding) {
|
||||||
|
return encode(source, 0, source.length, WEBSAFE_ALPHABET, doPadding);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encodes a byte array into Base64 notation.
|
||||||
|
*
|
||||||
|
* @param source the data to convert
|
||||||
|
* @param off offset in array where conversion should begin
|
||||||
|
* @param len length of data to convert
|
||||||
|
* @param alphabet the encoding alphabet
|
||||||
|
* @param doPadding is {@code true} to pad result with '=' chars
|
||||||
|
* if it does not fall on 3 byte boundaries
|
||||||
|
* @since 1.4
|
||||||
|
*/
|
||||||
|
public static String encode(byte[] source, int off, int len, byte[] alphabet,
|
||||||
|
boolean doPadding) {
|
||||||
|
byte[] outBuff = encode(source, off, len, alphabet, Integer.MAX_VALUE);
|
||||||
|
int outLen = outBuff.length;
|
||||||
|
|
||||||
|
// If doPadding is false, set length to truncate '='
|
||||||
|
// padding characters
|
||||||
|
while (doPadding == false && outLen > 0) {
|
||||||
|
if (outBuff[outLen - 1] != '=') {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
outLen -= 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new String(outBuff, 0, outLen);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encodes a byte array into Base64 notation.
|
||||||
|
*
|
||||||
|
* @param source the data to convert
|
||||||
|
* @param off offset in array where conversion should begin
|
||||||
|
* @param len length of data to convert
|
||||||
|
* @param alphabet is the encoding alphabet
|
||||||
|
* @param maxLineLength maximum length of one line.
|
||||||
|
* @return the BASE64-encoded byte array
|
||||||
|
*/
|
||||||
|
public static byte[] encode(byte[] source, int off, int len, byte[] alphabet,
|
||||||
|
int maxLineLength) {
|
||||||
|
int lenDiv3 = (len + 2) / 3; // ceil(len / 3)
|
||||||
|
int len43 = lenDiv3 * 4;
|
||||||
|
byte[] outBuff = new byte[len43 // Main 4:3
|
||||||
|
+ (len43 / maxLineLength)]; // New lines
|
||||||
|
|
||||||
|
int d = 0;
|
||||||
|
int e = 0;
|
||||||
|
int len2 = len - 2;
|
||||||
|
int lineLength = 0;
|
||||||
|
for (; d < len2; d += 3, e += 4) {
|
||||||
|
|
||||||
|
// The following block of code is the same as
|
||||||
|
// encode3to4( source, d + off, 3, outBuff, e, alphabet );
|
||||||
|
// but inlined for faster encoding (~20% improvement)
|
||||||
|
int inBuff =
|
||||||
|
((source[d + off] << 24) >>> 8)
|
||||||
|
| ((source[d + 1 + off] << 24) >>> 16)
|
||||||
|
| ((source[d + 2 + off] << 24) >>> 24);
|
||||||
|
outBuff[e] = alphabet[(inBuff >>> 18)];
|
||||||
|
outBuff[e + 1] = alphabet[(inBuff >>> 12) & 0x3f];
|
||||||
|
outBuff[e + 2] = alphabet[(inBuff >>> 6) & 0x3f];
|
||||||
|
outBuff[e + 3] = alphabet[(inBuff) & 0x3f];
|
||||||
|
|
||||||
|
lineLength += 4;
|
||||||
|
if (lineLength == maxLineLength) {
|
||||||
|
outBuff[e + 4] = NEW_LINE;
|
||||||
|
e++;
|
||||||
|
lineLength = 0;
|
||||||
|
} // end if: end of line
|
||||||
|
} // end for: each piece of array
|
||||||
|
|
||||||
|
if (d < len) {
|
||||||
|
encode3to4(source, d + off, len - d, outBuff, e, alphabet);
|
||||||
|
|
||||||
|
lineLength += 4;
|
||||||
|
if (lineLength == maxLineLength) {
|
||||||
|
// Add a last newline
|
||||||
|
outBuff[e + 4] = NEW_LINE;
|
||||||
|
e++;
|
||||||
|
}
|
||||||
|
e += 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
assert (e == outBuff.length);
|
||||||
|
return outBuff;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* ******** D E C O D I N G M E T H O D S ******** */
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decodes four bytes from array <var>source</var>
|
||||||
|
* and writes the resulting bytes (up to three of them)
|
||||||
|
* to <var>destination</var>.
|
||||||
|
* The source and destination arrays can be manipulated
|
||||||
|
* anywhere along their length by specifying
|
||||||
|
* <var>srcOffset</var> and <var>destOffset</var>.
|
||||||
|
* This method does not check to make sure your arrays
|
||||||
|
* are large enough to accommodate <var>srcOffset</var> + 4 for
|
||||||
|
* the <var>source</var> array or <var>destOffset</var> + 3 for
|
||||||
|
* the <var>destination</var> array.
|
||||||
|
* This method returns the actual number of bytes that
|
||||||
|
* were converted from the Base64 encoding.
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* @param source the array to convert
|
||||||
|
* @param srcOffset the index where conversion begins
|
||||||
|
* @param destination the array to hold the conversion
|
||||||
|
* @param destOffset the index where output will be put
|
||||||
|
* @param decodabet the decodabet for decoding Base64 content
|
||||||
|
* @return the number of decoded bytes converted
|
||||||
|
* @since 1.3
|
||||||
|
*/
|
||||||
|
private static int decode4to3(byte[] source, int srcOffset,
|
||||||
|
byte[] destination, int destOffset, byte[] decodabet) {
|
||||||
|
// Example: Dk==
|
||||||
|
if (source[srcOffset + 2] == EQUALS_SIGN) {
|
||||||
|
int outBuff =
|
||||||
|
((decodabet[source[srcOffset]] << 24) >>> 6)
|
||||||
|
| ((decodabet[source[srcOffset + 1]] << 24) >>> 12);
|
||||||
|
|
||||||
|
destination[destOffset] = (byte) (outBuff >>> 16);
|
||||||
|
return 1;
|
||||||
|
} else if (source[srcOffset + 3] == EQUALS_SIGN) {
|
||||||
|
// Example: DkL=
|
||||||
|
int outBuff =
|
||||||
|
((decodabet[source[srcOffset]] << 24) >>> 6)
|
||||||
|
| ((decodabet[source[srcOffset + 1]] << 24) >>> 12)
|
||||||
|
| ((decodabet[source[srcOffset + 2]] << 24) >>> 18);
|
||||||
|
|
||||||
|
destination[destOffset] = (byte) (outBuff >>> 16);
|
||||||
|
destination[destOffset + 1] = (byte) (outBuff >>> 8);
|
||||||
|
return 2;
|
||||||
|
} else {
|
||||||
|
// Example: DkLE
|
||||||
|
int outBuff =
|
||||||
|
((decodabet[source[srcOffset]] << 24) >>> 6)
|
||||||
|
| ((decodabet[source[srcOffset + 1]] << 24) >>> 12)
|
||||||
|
| ((decodabet[source[srcOffset + 2]] << 24) >>> 18)
|
||||||
|
| ((decodabet[source[srcOffset + 3]] << 24) >>> 24);
|
||||||
|
|
||||||
|
destination[destOffset] = (byte) (outBuff >> 16);
|
||||||
|
destination[destOffset + 1] = (byte) (outBuff >> 8);
|
||||||
|
destination[destOffset + 2] = (byte) (outBuff);
|
||||||
|
return 3;
|
||||||
|
}
|
||||||
|
} // end decodeToBytes
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decodes data from Base64 notation.
|
||||||
|
*
|
||||||
|
* @param s the string to decode (decoded in default encoding)
|
||||||
|
* @return the decoded data
|
||||||
|
* @since 1.4
|
||||||
|
*/
|
||||||
|
public static byte[] decode(String s) throws Base64DecoderException {
|
||||||
|
byte[] bytes = s.getBytes();
|
||||||
|
return decode(bytes, 0, bytes.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decodes data from web safe Base64 notation.
|
||||||
|
* Web safe encoding uses '-' instead of '+', '_' instead of '/'
|
||||||
|
*
|
||||||
|
* @param s the string to decode (decoded in default encoding)
|
||||||
|
* @return the decoded data
|
||||||
|
*/
|
||||||
|
public static byte[] decodeWebSafe(String s) throws Base64DecoderException {
|
||||||
|
byte[] bytes = s.getBytes();
|
||||||
|
return decodeWebSafe(bytes, 0, bytes.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decodes Base64 content in byte array format and returns
|
||||||
|
* the decoded byte array.
|
||||||
|
*
|
||||||
|
* @param source The Base64 encoded data
|
||||||
|
* @return decoded data
|
||||||
|
* @since 1.3
|
||||||
|
* @throws Base64DecoderException
|
||||||
|
*/
|
||||||
|
public static byte[] decode(byte[] source) throws Base64DecoderException {
|
||||||
|
return decode(source, 0, source.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decodes web safe Base64 content in byte array format and returns
|
||||||
|
* the decoded data.
|
||||||
|
* Web safe encoding uses '-' instead of '+', '_' instead of '/'
|
||||||
|
*
|
||||||
|
* @param source the string to decode (decoded in default encoding)
|
||||||
|
* @return the decoded data
|
||||||
|
*/
|
||||||
|
public static byte[] decodeWebSafe(byte[] source)
|
||||||
|
throws Base64DecoderException {
|
||||||
|
return decodeWebSafe(source, 0, source.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decodes Base64 content in byte array format and returns
|
||||||
|
* the decoded byte array.
|
||||||
|
*
|
||||||
|
* @param source the Base64 encoded data
|
||||||
|
* @param off the offset of where to begin decoding
|
||||||
|
* @param len the length of characters to decode
|
||||||
|
* @return decoded data
|
||||||
|
* @since 1.3
|
||||||
|
* @throws Base64DecoderException
|
||||||
|
*/
|
||||||
|
public static byte[] decode(byte[] source, int off, int len)
|
||||||
|
throws Base64DecoderException {
|
||||||
|
return decode(source, off, len, DECODABET);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decodes web safe Base64 content in byte array format and returns
|
||||||
|
* the decoded byte array.
|
||||||
|
* Web safe encoding uses '-' instead of '+', '_' instead of '/'
|
||||||
|
*
|
||||||
|
* @param source the Base64 encoded data
|
||||||
|
* @param off the offset of where to begin decoding
|
||||||
|
* @param len the length of characters to decode
|
||||||
|
* @return decoded data
|
||||||
|
*/
|
||||||
|
public static byte[] decodeWebSafe(byte[] source, int off, int len)
|
||||||
|
throws Base64DecoderException {
|
||||||
|
return decode(source, off, len, WEBSAFE_DECODABET);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decodes Base64 content using the supplied decodabet and returns
|
||||||
|
* the decoded byte array.
|
||||||
|
*
|
||||||
|
* @param source the Base64 encoded data
|
||||||
|
* @param off the offset of where to begin decoding
|
||||||
|
* @param len the length of characters to decode
|
||||||
|
* @param decodabet the decodabet for decoding Base64 content
|
||||||
|
* @return decoded data
|
||||||
|
*/
|
||||||
|
public static byte[] decode(byte[] source, int off, int len, byte[] decodabet)
|
||||||
|
throws Base64DecoderException {
|
||||||
|
int len34 = len * 3 / 4;
|
||||||
|
byte[] outBuff = new byte[2 + len34]; // Upper limit on size of output
|
||||||
|
int outBuffPosn = 0;
|
||||||
|
|
||||||
|
byte[] b4 = new byte[4];
|
||||||
|
int b4Posn = 0;
|
||||||
|
int i = 0;
|
||||||
|
byte sbiCrop = 0;
|
||||||
|
byte sbiDecode = 0;
|
||||||
|
for (i = 0; i < len; i++) {
|
||||||
|
sbiCrop = (byte) (source[i + off] & 0x7f); // Only the low seven bits
|
||||||
|
sbiDecode = decodabet[sbiCrop];
|
||||||
|
|
||||||
|
if (sbiDecode >= WHITE_SPACE_ENC) { // White space Equals sign or better
|
||||||
|
if (sbiDecode >= EQUALS_SIGN_ENC) {
|
||||||
|
// An equals sign (for padding) must not occur at position 0 or 1
|
||||||
|
// and must be the last byte[s] in the encoded value
|
||||||
|
if (sbiCrop == EQUALS_SIGN) {
|
||||||
|
int bytesLeft = len - i;
|
||||||
|
byte lastByte = (byte) (source[len - 1 + off] & 0x7f);
|
||||||
|
if (b4Posn == 0 || b4Posn == 1) {
|
||||||
|
throw new Base64DecoderException(
|
||||||
|
"invalid padding byte '=' at byte offset " + i);
|
||||||
|
} else if ((b4Posn == 3 && bytesLeft > 2)
|
||||||
|
|| (b4Posn == 4 && bytesLeft > 1)) {
|
||||||
|
throw new Base64DecoderException(
|
||||||
|
"padding byte '=' falsely signals end of encoded value "
|
||||||
|
+ "at offset " + i);
|
||||||
|
} else if (lastByte != EQUALS_SIGN && lastByte != NEW_LINE) {
|
||||||
|
throw new Base64DecoderException(
|
||||||
|
"encoded value has invalid trailing byte");
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
b4[b4Posn++] = sbiCrop;
|
||||||
|
if (b4Posn == 4) {
|
||||||
|
outBuffPosn += decode4to3(b4, 0, outBuff, outBuffPosn, decodabet);
|
||||||
|
b4Posn = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new Base64DecoderException("Bad Base64 input character at " + i
|
||||||
|
+ ": " + source[i + off] + "(decimal)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Because web safe encoding allows non padding base64 encodes, we
|
||||||
|
// need to pad the rest of the b4 buffer with equal signs when
|
||||||
|
// b4Posn != 0. There can be at most 2 equal signs at the end of
|
||||||
|
// four characters, so the b4 buffer must have two or three
|
||||||
|
// characters. This also catches the case where the input is
|
||||||
|
// padded with EQUALS_SIGN
|
||||||
|
if (b4Posn != 0) {
|
||||||
|
if (b4Posn == 1) {
|
||||||
|
throw new Base64DecoderException("single trailing character at offset "
|
||||||
|
+ (len - 1));
|
||||||
|
}
|
||||||
|
b4[b4Posn++] = EQUALS_SIGN;
|
||||||
|
outBuffPosn += decode4to3(b4, 0, outBuff, outBuffPosn, decodabet);
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] out = new byte[outBuffPosn];
|
||||||
|
System.arraycopy(outBuff, 0, out, 0, outBuffPosn);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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; }
|
||||||
|
}
|
||||||
@@ -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.
|
||||||
|
* <p>
|
||||||
|
* After instantiating, you must perform setup in order to start using the object.
|
||||||
|
* To perform setup, call the {@link #startSetup} method and provide a listener;
|
||||||
|
* that listener will be notified when setup is complete, after which (and not before)
|
||||||
|
* you may call other methods.
|
||||||
|
* <p>
|
||||||
|
* After setup is complete, you will typically want to request an inventory of owned
|
||||||
|
* items and subscriptions. See {@link #queryInventory}, {@link #queryInventoryAsync}
|
||||||
|
* and related methods.
|
||||||
|
* <p>
|
||||||
|
* When you are done with this object, don't forget to call {@link #dispose}
|
||||||
|
* to ensure proper cleanup. This object holds a binding to the in-app billing
|
||||||
|
* service, which will leak unless you dispose of it correctly. If you created
|
||||||
|
* the object on an Activity's onCreate method, then the recommended
|
||||||
|
* place to dispose of it is the Activity's onDestroy method.
|
||||||
|
* <p>
|
||||||
|
* A note about threading: When using this object from a background thread, you may
|
||||||
|
* call the blocking versions of methods; when using from a UI thread, call
|
||||||
|
* only the asynchronous versions and handle the results via callbacks.
|
||||||
|
* Also, notice that you can only call one asynchronous operation at a time;
|
||||||
|
* attempting to start a second asynchronous operation while the first one
|
||||||
|
* has not yet completed will result in an exception being thrown.
|
||||||
|
*
|
||||||
|
* @author Bruno Oliveira (Google)
|
||||||
|
*/
|
||||||
|
public class IabHelper {
|
||||||
|
// Is debug logging enabled?
|
||||||
|
boolean mDebugLog = false;
|
||||||
|
String mDebugTag = "IabHelper";
|
||||||
|
|
||||||
|
// Is setup done?
|
||||||
|
boolean mSetupDone = false;
|
||||||
|
|
||||||
|
// Has this object been disposed of? (If so, we should ignore callbacks, etc)
|
||||||
|
boolean mDisposed = false;
|
||||||
|
|
||||||
|
// Are subscriptions supported?
|
||||||
|
boolean mSubscriptionsSupported = false;
|
||||||
|
|
||||||
|
// Is an asynchronous operation in progress?
|
||||||
|
// (only one at a time can be in progress)
|
||||||
|
boolean mAsyncInProgress = false;
|
||||||
|
|
||||||
|
// (for logging/debugging)
|
||||||
|
// if mAsyncInProgress == true, what asynchronous operation is in progress?
|
||||||
|
String mAsyncOperation = "";
|
||||||
|
|
||||||
|
// Context we were passed during initialization
|
||||||
|
Context mContext;
|
||||||
|
|
||||||
|
// Connection to the service
|
||||||
|
IInAppBillingService mService;
|
||||||
|
ServiceConnection mServiceConn;
|
||||||
|
|
||||||
|
// The request code used to launch purchase flow
|
||||||
|
int mRequestCode;
|
||||||
|
|
||||||
|
// The item type of the current purchase flow
|
||||||
|
String mPurchasingItemType;
|
||||||
|
|
||||||
|
// Public key for verifying signature, in base64 encoding
|
||||||
|
String mSignatureBase64 = null;
|
||||||
|
|
||||||
|
// Billing response codes
|
||||||
|
public static final int BILLING_RESPONSE_RESULT_OK = 0;
|
||||||
|
public static final int BILLING_RESPONSE_RESULT_USER_CANCELED = 1;
|
||||||
|
public static final int BILLING_RESPONSE_RESULT_BILLING_UNAVAILABLE = 3;
|
||||||
|
public static final int BILLING_RESPONSE_RESULT_ITEM_UNAVAILABLE = 4;
|
||||||
|
public static final int BILLING_RESPONSE_RESULT_DEVELOPER_ERROR = 5;
|
||||||
|
public static final int BILLING_RESPONSE_RESULT_ERROR = 6;
|
||||||
|
public static final int BILLING_RESPONSE_RESULT_ITEM_ALREADY_OWNED = 7;
|
||||||
|
public static final int BILLING_RESPONSE_RESULT_ITEM_NOT_OWNED = 8;
|
||||||
|
|
||||||
|
// IAB Helper error codes
|
||||||
|
public static final int IABHELPER_ERROR_BASE = -1000;
|
||||||
|
public static final int IABHELPER_REMOTE_EXCEPTION = -1001;
|
||||||
|
public static final int IABHELPER_BAD_RESPONSE = -1002;
|
||||||
|
public static final int IABHELPER_VERIFICATION_FAILED = -1003;
|
||||||
|
public static final int IABHELPER_SEND_INTENT_FAILED = -1004;
|
||||||
|
public static final int IABHELPER_USER_CANCELLED = -1005;
|
||||||
|
public static final int IABHELPER_UNKNOWN_PURCHASE_RESPONSE = -1006;
|
||||||
|
public static final int IABHELPER_MISSING_TOKEN = -1007;
|
||||||
|
public static final int IABHELPER_UNKNOWN_ERROR = -1008;
|
||||||
|
public static final int IABHELPER_SUBSCRIPTIONS_NOT_AVAILABLE = -1009;
|
||||||
|
public static final int IABHELPER_INVALID_CONSUMPTION = -1010;
|
||||||
|
|
||||||
|
// Keys for the responses from InAppBillingService
|
||||||
|
public static final String RESPONSE_CODE = "RESPONSE_CODE";
|
||||||
|
public static final String RESPONSE_GET_SKU_DETAILS_LIST = "DETAILS_LIST";
|
||||||
|
public static final String RESPONSE_BUY_INTENT = "BUY_INTENT";
|
||||||
|
public static final String RESPONSE_INAPP_PURCHASE_DATA = "INAPP_PURCHASE_DATA";
|
||||||
|
public static final String RESPONSE_INAPP_SIGNATURE = "INAPP_DATA_SIGNATURE";
|
||||||
|
public static final String RESPONSE_INAPP_ITEM_LIST = "INAPP_PURCHASE_ITEM_LIST";
|
||||||
|
public static final String RESPONSE_INAPP_PURCHASE_DATA_LIST = "INAPP_PURCHASE_DATA_LIST";
|
||||||
|
public static final String RESPONSE_INAPP_SIGNATURE_LIST = "INAPP_DATA_SIGNATURE_LIST";
|
||||||
|
public static final String INAPP_CONTINUATION_TOKEN = "INAPP_CONTINUATION_TOKEN";
|
||||||
|
|
||||||
|
// Item types
|
||||||
|
public static final String ITEM_TYPE_INAPP = "inapp";
|
||||||
|
public static final String ITEM_TYPE_SUBS = "subs";
|
||||||
|
|
||||||
|
// some fields on the getSkuDetails response bundle
|
||||||
|
public static final String GET_SKU_DETAILS_ITEM_LIST = "ITEM_ID_LIST";
|
||||||
|
public static final String GET_SKU_DETAILS_ITEM_TYPE_LIST = "ITEM_TYPE_LIST";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an instance. After creation, it will not yet be ready to use. You must perform
|
||||||
|
* setup by calling {@link #startSetup} and wait for setup to complete. This constructor does not
|
||||||
|
* block and is safe to call from a UI thread.
|
||||||
|
*
|
||||||
|
* @param ctx Your application or Activity context. Needed to bind to the in-app billing service.
|
||||||
|
* @param base64PublicKey Your application's public key, encoded in base64.
|
||||||
|
* This is used for verification of purchase signatures. You can find your app's base64-encoded
|
||||||
|
* public key in your application's page on Google Play Developer Console. Note that this
|
||||||
|
* is NOT your "developer public key".
|
||||||
|
*/
|
||||||
|
public IabHelper(Context ctx, String base64PublicKey) {
|
||||||
|
mContext = ctx.getApplicationContext();
|
||||||
|
mSignatureBase64 = base64PublicKey;
|
||||||
|
logDebug("IAB helper created.");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enables or disable debug logging through LogCat.
|
||||||
|
*/
|
||||||
|
public void enableDebugLogging(boolean enable, String tag) {
|
||||||
|
checkNotDisposed();
|
||||||
|
mDebugLog = enable;
|
||||||
|
mDebugTag = tag;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void enableDebugLogging(boolean enable) {
|
||||||
|
checkNotDisposed();
|
||||||
|
mDebugLog = enable;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback for setup process. This listener's {@link #onIabSetupFinished} method is called
|
||||||
|
* when the setup process is complete.
|
||||||
|
*/
|
||||||
|
public interface OnIabSetupFinishedListener {
|
||||||
|
/**
|
||||||
|
* Called to notify that setup is complete.
|
||||||
|
*
|
||||||
|
* @param result The result of the setup process.
|
||||||
|
*/
|
||||||
|
void onIabSetupFinished(IabResult result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts the setup process. This will start up the setup process asynchronously.
|
||||||
|
* You will be notified through the listener when the setup process is complete.
|
||||||
|
* This method is safe to call from a UI thread.
|
||||||
|
*
|
||||||
|
* @param listener The listener to notify when the setup process is complete.
|
||||||
|
*/
|
||||||
|
public void startSetup(final OnIabSetupFinishedListener listener) {
|
||||||
|
// If already set up, can't do it again.
|
||||||
|
checkNotDisposed();
|
||||||
|
if (mSetupDone) throw new IllegalStateException("IAB helper is already set up.");
|
||||||
|
|
||||||
|
// Connection to IAB service
|
||||||
|
logDebug("Starting in-app billing setup.");
|
||||||
|
mServiceConn = new ServiceConnection() {
|
||||||
|
@Override
|
||||||
|
public void onServiceDisconnected(ComponentName name) {
|
||||||
|
logDebug("Billing service disconnected.");
|
||||||
|
mService = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onServiceConnected(ComponentName name, IBinder service) {
|
||||||
|
if (mDisposed) return;
|
||||||
|
logDebug("Billing service connected.");
|
||||||
|
mService = IInAppBillingService.Stub.asInterface(service);
|
||||||
|
String packageName = mContext.getPackageName();
|
||||||
|
try {
|
||||||
|
logDebug("Checking for in-app billing 3 support.");
|
||||||
|
|
||||||
|
// check for in-app billing v3 support
|
||||||
|
int response = mService.isBillingSupported(3, packageName, ITEM_TYPE_INAPP);
|
||||||
|
if (response != BILLING_RESPONSE_RESULT_OK) {
|
||||||
|
if (listener != null) listener.onIabSetupFinished(new IabResult(response,
|
||||||
|
"Error checking for billing v3 support."));
|
||||||
|
|
||||||
|
// if in-app purchases aren't supported, neither are subscriptions.
|
||||||
|
mSubscriptionsSupported = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
logDebug("In-app billing version 3 supported for " + packageName);
|
||||||
|
|
||||||
|
// check for v3 subscriptions support
|
||||||
|
response = mService.isBillingSupported(3, packageName, ITEM_TYPE_SUBS);
|
||||||
|
if (response == BILLING_RESPONSE_RESULT_OK) {
|
||||||
|
logDebug("Subscriptions AVAILABLE.");
|
||||||
|
mSubscriptionsSupported = true;
|
||||||
|
} else {
|
||||||
|
logDebug("Subscriptions NOT AVAILABLE. Response: " + response);
|
||||||
|
}
|
||||||
|
|
||||||
|
mSetupDone = true;
|
||||||
|
} catch (RemoteException e) {
|
||||||
|
if (listener != null) {
|
||||||
|
listener.onIabSetupFinished(new IabResult(IABHELPER_REMOTE_EXCEPTION,
|
||||||
|
"RemoteException while setting up in-app billing."));
|
||||||
|
}
|
||||||
|
e.printStackTrace();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (listener != null) {
|
||||||
|
listener.onIabSetupFinished(new IabResult(BILLING_RESPONSE_RESULT_OK, "Setup successful."));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Intent serviceIntent = new Intent("ir.cafebazaar.pardakht.InAppBillingService.BIND");
|
||||||
|
// serviceIntent.setPackage("com.farsitel.bazaar");
|
||||||
|
Intent serviceIntent = new Intent("com.android.vending.billing.InAppBillingService.BIND");
|
||||||
|
serviceIntent.setPackage("com.android.vending");
|
||||||
|
if (!mContext.getPackageManager().queryIntentServices(serviceIntent, 0).isEmpty()) {
|
||||||
|
// service available to handle that Intent
|
||||||
|
mContext.bindService(serviceIntent, mServiceConn, Context.BIND_AUTO_CREATE);
|
||||||
|
} else {
|
||||||
|
// no service available to handle that Intent
|
||||||
|
if (listener != null) {
|
||||||
|
listener.onIabSetupFinished(
|
||||||
|
new IabResult(BILLING_RESPONSE_RESULT_BILLING_UNAVAILABLE,
|
||||||
|
"Billing service unavailable on device."));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dispose of object, releasing resources. It's very important to call this
|
||||||
|
* method when you are done with this object. It will release any resources
|
||||||
|
* used by it such as service connections. Naturally, once the object is
|
||||||
|
* disposed of, it can't be used again.
|
||||||
|
*/
|
||||||
|
public void dispose() {
|
||||||
|
logDebug("Disposing.");
|
||||||
|
mSetupDone = false;
|
||||||
|
if (mServiceConn != null) {
|
||||||
|
logDebug("Unbinding from service.");
|
||||||
|
if (mContext != null) mContext.unbindService(mServiceConn);
|
||||||
|
}
|
||||||
|
mDisposed = true;
|
||||||
|
mContext = null;
|
||||||
|
mServiceConn = null;
|
||||||
|
mService = null;
|
||||||
|
mPurchaseListener = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void checkNotDisposed() {
|
||||||
|
if (mDisposed)
|
||||||
|
throw new IllegalStateException("IabHelper was disposed of, so it cannot be used.");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether subscriptions are supported.
|
||||||
|
*/
|
||||||
|
public boolean subscriptionsSupported() {
|
||||||
|
checkNotDisposed();
|
||||||
|
return mSubscriptionsSupported;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback that notifies when a purchase is finished.
|
||||||
|
*/
|
||||||
|
public interface OnIabPurchaseFinishedListener {
|
||||||
|
/**
|
||||||
|
* Called to notify that an in-app purchase finished. If the purchase was successful,
|
||||||
|
* then the sku parameter specifies which item was purchased. If the purchase failed,
|
||||||
|
* the sku and extraData parameters may or may not be null, depending on how far the purchase
|
||||||
|
* process went.
|
||||||
|
*
|
||||||
|
* @param result The result of the purchase.
|
||||||
|
* @param info The purchase information (null if purchase failed)
|
||||||
|
*/
|
||||||
|
void onIabPurchaseFinished(IabResult result, Purchase info);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The listener registered on launchPurchaseFlow, which we have to call back when
|
||||||
|
// the purchase finishes
|
||||||
|
OnIabPurchaseFinishedListener mPurchaseListener;
|
||||||
|
|
||||||
|
public void launchPurchaseFlow(Activity act, String sku, int requestCode, OnIabPurchaseFinishedListener listener) {
|
||||||
|
launchPurchaseFlow(act, sku, requestCode, listener, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void launchPurchaseFlow(Activity act, String sku, int requestCode,
|
||||||
|
OnIabPurchaseFinishedListener listener, String extraData) {
|
||||||
|
launchPurchaseFlow(act, sku, ITEM_TYPE_INAPP, requestCode, listener, extraData);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void launchSubscriptionPurchaseFlow(Activity act, String sku, int requestCode,
|
||||||
|
OnIabPurchaseFinishedListener listener) {
|
||||||
|
launchSubscriptionPurchaseFlow(act, sku, requestCode, listener, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void launchSubscriptionPurchaseFlow(Activity act, String sku, int requestCode,
|
||||||
|
OnIabPurchaseFinishedListener listener, String extraData) {
|
||||||
|
launchPurchaseFlow(act, sku, ITEM_TYPE_SUBS, requestCode, listener, extraData);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initiate the UI flow for an in-app purchase. Call this method to initiate an in-app purchase,
|
||||||
|
* which will involve bringing up the Google Play screen. The calling activity will be paused while
|
||||||
|
* the user interacts with Google Play, and the result will be delivered via the activity's
|
||||||
|
* {@link android.app.Activity#onActivityResult} method, at which point you must call
|
||||||
|
* this object's {@link #handleActivityResult} method to continue the purchase flow. This method
|
||||||
|
* MUST be called from the UI thread of the Activity.
|
||||||
|
*
|
||||||
|
* @param act The calling activity.
|
||||||
|
* @param sku The sku of the item to purchase.
|
||||||
|
* @param itemType indicates if it's a product or a subscription (ITEM_TYPE_INAPP or ITEM_TYPE_SUBS)
|
||||||
|
* @param requestCode A request code (to differentiate from other responses --
|
||||||
|
* as in {@link android.app.Activity#startActivityForResult}).
|
||||||
|
* @param listener The listener to notify when the purchase process finishes
|
||||||
|
* @param extraData Extra data (developer payload), which will be returned with the purchase data
|
||||||
|
* when the purchase completes. This extra data will be permanently bound to that purchase
|
||||||
|
* and will always be returned when the purchase is queried.
|
||||||
|
*/
|
||||||
|
public void launchPurchaseFlow(Activity act, String sku, String itemType, int requestCode,
|
||||||
|
OnIabPurchaseFinishedListener listener, String extraData) {
|
||||||
|
checkNotDisposed();
|
||||||
|
checkSetupDone("launchPurchaseFlow");
|
||||||
|
flagStartAsync("launchPurchaseFlow");
|
||||||
|
IabResult result;
|
||||||
|
|
||||||
|
if (itemType.equals(ITEM_TYPE_SUBS) && !mSubscriptionsSupported) {
|
||||||
|
IabResult r = new IabResult(IABHELPER_SUBSCRIPTIONS_NOT_AVAILABLE,
|
||||||
|
"Subscriptions are not available.");
|
||||||
|
flagEndAsync();
|
||||||
|
if (listener != null) listener.onIabPurchaseFinished(r, null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
logDebug("Constructing buy intent for " + sku + ", item type: " + itemType);
|
||||||
|
Bundle buyIntentBundle = mService.getBuyIntent(3, mContext.getPackageName(), sku, itemType, extraData);
|
||||||
|
int response = getResponseCodeFromBundle(buyIntentBundle);
|
||||||
|
if (response != BILLING_RESPONSE_RESULT_OK) {
|
||||||
|
logError("Unable to buy item, Error response: " + getResponseDesc(response));
|
||||||
|
flagEndAsync();
|
||||||
|
result = new IabResult(response, "Unable to buy item");
|
||||||
|
if (listener != null) listener.onIabPurchaseFinished(result, null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
PendingIntent pendingIntent = buyIntentBundle.getParcelable(RESPONSE_BUY_INTENT);
|
||||||
|
logDebug("Launching buy intent for " + sku + ". Request code: " + requestCode);
|
||||||
|
mRequestCode = requestCode;
|
||||||
|
mPurchaseListener = listener;
|
||||||
|
mPurchasingItemType = itemType;
|
||||||
|
act.startIntentSenderForResult(pendingIntent.getIntentSender(),
|
||||||
|
requestCode, new Intent(),
|
||||||
|
Integer.valueOf(0), Integer.valueOf(0),
|
||||||
|
Integer.valueOf(0));
|
||||||
|
} catch (SendIntentException e) {
|
||||||
|
logError("SendIntentException while launching purchase flow for sku " + sku);
|
||||||
|
e.printStackTrace();
|
||||||
|
flagEndAsync();
|
||||||
|
|
||||||
|
result = new IabResult(IABHELPER_SEND_INTENT_FAILED, "Failed to send intent.");
|
||||||
|
if (listener != null) listener.onIabPurchaseFinished(result, null);
|
||||||
|
} catch (RemoteException e) {
|
||||||
|
logError("RemoteException while launching purchase flow for sku " + sku);
|
||||||
|
e.printStackTrace();
|
||||||
|
flagEndAsync();
|
||||||
|
|
||||||
|
result = new IabResult(IABHELPER_REMOTE_EXCEPTION, "Remote exception while starting purchase flow");
|
||||||
|
if (listener != null) listener.onIabPurchaseFinished(result, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles an activity result that's part of the purchase flow in in-app billing. If you
|
||||||
|
* are calling {@link #launchPurchaseFlow}, then you must call this method from your
|
||||||
|
* Activity's {@link android.app.Activity@onActivityResult} method. This method
|
||||||
|
* MUST be called from the UI thread of the Activity.
|
||||||
|
*
|
||||||
|
* @param requestCode The requestCode as you received it.
|
||||||
|
* @param resultCode The resultCode as you received it.
|
||||||
|
* @param data The data (Intent) as you received it.
|
||||||
|
* @return Returns true if the result was related to a purchase flow and was handled;
|
||||||
|
* false if the result was not related to a purchase, in which case you should
|
||||||
|
* handle it normally.
|
||||||
|
*/
|
||||||
|
public boolean handleActivityResult(int requestCode, int resultCode, Intent data) {
|
||||||
|
IabResult result;
|
||||||
|
if (requestCode != mRequestCode) return false;
|
||||||
|
|
||||||
|
checkNotDisposed();
|
||||||
|
checkSetupDone("handleActivityResult");
|
||||||
|
|
||||||
|
// end of async purchase operation that started on launchPurchaseFlow
|
||||||
|
flagEndAsync();
|
||||||
|
|
||||||
|
if (data == null) {
|
||||||
|
logError("Null data in IAB activity result.");
|
||||||
|
result = new IabResult(IABHELPER_BAD_RESPONSE, "Null data in IAB result");
|
||||||
|
if (mPurchaseListener != null) mPurchaseListener.onIabPurchaseFinished(result, null);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
int responseCode = getResponseCodeFromIntent(data);
|
||||||
|
String purchaseData = data.getStringExtra(RESPONSE_INAPP_PURCHASE_DATA);
|
||||||
|
String dataSignature = data.getStringExtra(RESPONSE_INAPP_SIGNATURE);
|
||||||
|
|
||||||
|
if (resultCode == Activity.RESULT_OK && responseCode == BILLING_RESPONSE_RESULT_OK) {
|
||||||
|
logDebug("Successful resultcode from purchase activity.");
|
||||||
|
logDebug("Purchase data: " + purchaseData);
|
||||||
|
logDebug("Data signature: " + dataSignature);
|
||||||
|
logDebug("Extras: " + data.getExtras());
|
||||||
|
logDebug("Expected item type: " + mPurchasingItemType);
|
||||||
|
|
||||||
|
if (purchaseData == null || dataSignature == null) {
|
||||||
|
logError("BUG: either purchaseData or dataSignature is null.");
|
||||||
|
logDebug("Extras: " + data.getExtras().toString());
|
||||||
|
result = new IabResult(IABHELPER_UNKNOWN_ERROR, "IAB returned null purchaseData or dataSignature");
|
||||||
|
if (mPurchaseListener != null)
|
||||||
|
mPurchaseListener.onIabPurchaseFinished(result, null);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Purchase purchase = null;
|
||||||
|
try {
|
||||||
|
purchase = new Purchase(mPurchasingItemType, purchaseData, dataSignature);
|
||||||
|
String sku = purchase.getSku();
|
||||||
|
|
||||||
|
// Verify signature
|
||||||
|
if (!Security.verifyPurchase(mSignatureBase64, purchaseData, dataSignature)) {
|
||||||
|
logError("Purchase signature verification FAILED for sku " + sku);
|
||||||
|
result = new IabResult(IABHELPER_VERIFICATION_FAILED, "Signature verification failed for sku " + sku);
|
||||||
|
if (mPurchaseListener != null)
|
||||||
|
mPurchaseListener.onIabPurchaseFinished(result, purchase);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
logDebug("Purchase signature successfully verified.");
|
||||||
|
} catch (JSONException e) {
|
||||||
|
logError("Failed to parse purchase data.");
|
||||||
|
e.printStackTrace();
|
||||||
|
result = new IabResult(IABHELPER_BAD_RESPONSE, "Failed to parse purchase data.");
|
||||||
|
if (mPurchaseListener != null)
|
||||||
|
mPurchaseListener.onIabPurchaseFinished(result, null);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mPurchaseListener != null) {
|
||||||
|
mPurchaseListener.onIabPurchaseFinished(new IabResult(BILLING_RESPONSE_RESULT_OK, "Success"), purchase);
|
||||||
|
}
|
||||||
|
} else if (resultCode == Activity.RESULT_OK) {
|
||||||
|
// result code was OK, but in-app billing response was not OK.
|
||||||
|
logDebug("Result code was OK but in-app billing response was not OK: " + getResponseDesc(responseCode));
|
||||||
|
if (mPurchaseListener != null) {
|
||||||
|
result = new IabResult(responseCode, "Problem purchashing item.");
|
||||||
|
mPurchaseListener.onIabPurchaseFinished(result, null);
|
||||||
|
}
|
||||||
|
} else if (resultCode == Activity.RESULT_CANCELED) {
|
||||||
|
logDebug("Purchase canceled - Response: " + getResponseDesc(responseCode));
|
||||||
|
result = new IabResult(IABHELPER_USER_CANCELLED, "User canceled.");
|
||||||
|
if (mPurchaseListener != null) mPurchaseListener.onIabPurchaseFinished(result, null);
|
||||||
|
} else {
|
||||||
|
logError("Purchase failed. Result code: " + Integer.toString(resultCode)
|
||||||
|
+ ". Response: " + getResponseDesc(responseCode));
|
||||||
|
result = new IabResult(IABHELPER_UNKNOWN_PURCHASE_RESPONSE, "Unknown purchase response.");
|
||||||
|
if (mPurchaseListener != null) mPurchaseListener.onIabPurchaseFinished(result, null);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Inventory queryInventory(boolean querySkuDetails, List<String> moreSkus) throws IabException {
|
||||||
|
return queryInventory(querySkuDetails, moreSkus, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Queries the inventory. This will query all owned items from the server, as well as
|
||||||
|
* information on additional skus, if specified. This method may block or take long to execute.
|
||||||
|
* Do not call from a UI thread. For that, use the non-blocking version {@link #refreshInventoryAsync}.
|
||||||
|
*
|
||||||
|
* @param querySkuDetails if true, SKU details (price, description, etc) will be queried as well
|
||||||
|
* as purchase information.
|
||||||
|
* @param moreItemSkus additional PRODUCT skus to query information on, regardless of ownership.
|
||||||
|
* Ignored if null or if querySkuDetails is false.
|
||||||
|
* @param moreSubsSkus additional SUBSCRIPTIONS skus to query information on, regardless of ownership.
|
||||||
|
* Ignored if null or if querySkuDetails is false.
|
||||||
|
* @throws IabException if a problem occurs while refreshing the inventory.
|
||||||
|
*/
|
||||||
|
public Inventory queryInventory(boolean querySkuDetails, List<String> moreItemSkus,
|
||||||
|
List<String> moreSubsSkus) throws IabException {
|
||||||
|
checkNotDisposed();
|
||||||
|
checkSetupDone("queryInventory");
|
||||||
|
try {
|
||||||
|
Inventory inv = new Inventory();
|
||||||
|
int r = queryPurchases(inv, ITEM_TYPE_INAPP);
|
||||||
|
if (r != BILLING_RESPONSE_RESULT_OK) {
|
||||||
|
throw new IabException(r, "Error refreshing inventory (querying owned items).");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (querySkuDetails) {
|
||||||
|
r = querySkuDetails(ITEM_TYPE_INAPP, inv, moreItemSkus);
|
||||||
|
if (r != BILLING_RESPONSE_RESULT_OK) {
|
||||||
|
throw new IabException(r, "Error refreshing inventory (querying prices of items).");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// if subscriptions are supported, then also query for subscriptions
|
||||||
|
if (mSubscriptionsSupported) {
|
||||||
|
r = queryPurchases(inv, ITEM_TYPE_SUBS);
|
||||||
|
if (r != BILLING_RESPONSE_RESULT_OK) {
|
||||||
|
throw new IabException(r, "Error refreshing inventory (querying owned subscriptions).");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (querySkuDetails) {
|
||||||
|
r = querySkuDetails(ITEM_TYPE_SUBS, inv, moreItemSkus);
|
||||||
|
if (r != BILLING_RESPONSE_RESULT_OK) {
|
||||||
|
throw new IabException(r, "Error refreshing inventory (querying prices of subscriptions).");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return inv;
|
||||||
|
} catch (RemoteException e) {
|
||||||
|
throw new IabException(IABHELPER_REMOTE_EXCEPTION, "Remote exception while refreshing inventory.", e);
|
||||||
|
} catch (JSONException e) {
|
||||||
|
throw new IabException(IABHELPER_BAD_RESPONSE, "Error parsing JSON response while refreshing inventory.", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Listener that notifies when an inventory query operation completes.
|
||||||
|
*/
|
||||||
|
public interface QueryInventoryFinishedListener {
|
||||||
|
/**
|
||||||
|
* Called to notify that an inventory query operation completed.
|
||||||
|
*
|
||||||
|
* @param result The result of the operation.
|
||||||
|
* @param inv The inventory.
|
||||||
|
*/
|
||||||
|
void onQueryInventoryFinished(IabResult result, Inventory inv);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asynchronous wrapper for inventory query. This will perform an inventory
|
||||||
|
* query as described in {@link #queryInventory}, but will do so asynchronously
|
||||||
|
* and call back the specified listener upon completion. This method is safe to
|
||||||
|
* call from a UI thread.
|
||||||
|
*
|
||||||
|
* @param querySkuDetails as in {@link #queryInventory}
|
||||||
|
* @param moreSkus as in {@link #queryInventory}
|
||||||
|
* @param listener The listener to notify when the refresh operation completes.
|
||||||
|
*/
|
||||||
|
public void queryInventoryAsync(final boolean querySkuDetails,
|
||||||
|
final List<String> moreSkus,
|
||||||
|
final QueryInventoryFinishedListener listener) {
|
||||||
|
final Handler handler = new Handler();
|
||||||
|
checkNotDisposed();
|
||||||
|
checkSetupDone("queryInventory");
|
||||||
|
flagStartAsync("refresh inventory");
|
||||||
|
(new Thread(new Runnable() {
|
||||||
|
public void run() {
|
||||||
|
IabResult result = new IabResult(BILLING_RESPONSE_RESULT_OK, "Inventory refresh successful.");
|
||||||
|
Inventory inv = null;
|
||||||
|
try {
|
||||||
|
inv = queryInventory(querySkuDetails, moreSkus);
|
||||||
|
} catch (IabException ex) {
|
||||||
|
result = ex.getResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
flagEndAsync();
|
||||||
|
|
||||||
|
final IabResult result_f = result;
|
||||||
|
final Inventory inv_f = inv;
|
||||||
|
if (!mDisposed && listener != null) {
|
||||||
|
handler.post(new Runnable() {
|
||||||
|
public void run() {
|
||||||
|
listener.onQueryInventoryFinished(result_f, inv_f);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})).start();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void queryInventoryAsync(QueryInventoryFinishedListener listener) {
|
||||||
|
queryInventoryAsync(true, null, listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void queryInventoryAsync(boolean querySkuDetails, QueryInventoryFinishedListener listener) {
|
||||||
|
queryInventoryAsync(querySkuDetails, null, listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Consumes a given in-app product. Consuming can only be done on an item
|
||||||
|
* that's owned, and as a result of consumption, the user will no longer own it.
|
||||||
|
* This method may block or take long to return. Do not call from the UI thread.
|
||||||
|
* For that, see {@link #consumeAsync}.
|
||||||
|
*
|
||||||
|
* @param itemInfo The PurchaseInfo that represents the item to consume.
|
||||||
|
* @throws IabException if there is a problem during consumption.
|
||||||
|
*/
|
||||||
|
void consume(Purchase itemInfo) throws IabException {
|
||||||
|
checkNotDisposed();
|
||||||
|
checkSetupDone("consume");
|
||||||
|
|
||||||
|
if (!itemInfo.mItemType.equals(ITEM_TYPE_INAPP)) {
|
||||||
|
throw new IabException(IABHELPER_INVALID_CONSUMPTION,
|
||||||
|
"Items of type '" + itemInfo.mItemType + "' can't be consumed.");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
String token = itemInfo.getToken();
|
||||||
|
String sku = itemInfo.getSku();
|
||||||
|
if (token == null || token.equals("")) {
|
||||||
|
logError("Can't consume " + sku + ". No token.");
|
||||||
|
throw new IabException(IABHELPER_MISSING_TOKEN, "PurchaseInfo is missing token for sku: "
|
||||||
|
+ sku + " " + itemInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
logDebug("Consuming sku: " + sku + ", token: " + token);
|
||||||
|
int response = mService.consumePurchase(3, mContext.getPackageName(), token);
|
||||||
|
if (response == BILLING_RESPONSE_RESULT_OK) {
|
||||||
|
logDebug("Successfully consumed sku: " + sku);
|
||||||
|
} else {
|
||||||
|
logDebug("Error consuming consuming sku " + sku + ". " + getResponseDesc(response));
|
||||||
|
throw new IabException(response, "Error consuming sku " + sku);
|
||||||
|
}
|
||||||
|
} catch (RemoteException e) {
|
||||||
|
throw new IabException(IABHELPER_REMOTE_EXCEPTION, "Remote exception while consuming. PurchaseInfo: " + itemInfo, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback that notifies when a consumption operation finishes.
|
||||||
|
*/
|
||||||
|
public interface OnConsumeFinishedListener {
|
||||||
|
/**
|
||||||
|
* Called to notify that a consumption has finished.
|
||||||
|
*
|
||||||
|
* @param purchase The purchase that was (or was to be) consumed.
|
||||||
|
* @param result The result of the consumption operation.
|
||||||
|
*/
|
||||||
|
void onConsumeFinished(Purchase purchase, IabResult result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback that notifies when a multi-item consumption operation finishes.
|
||||||
|
*/
|
||||||
|
public interface OnConsumeMultiFinishedListener {
|
||||||
|
/**
|
||||||
|
* Called to notify that a consumption of multiple items has finished.
|
||||||
|
*
|
||||||
|
* @param purchases The purchases that were (or were to be) consumed.
|
||||||
|
* @param results The results of each consumption operation, corresponding to each
|
||||||
|
* sku.
|
||||||
|
*/
|
||||||
|
void onConsumeMultiFinished(List<Purchase> purchases, List<IabResult> results);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asynchronous wrapper to item consumption. Works like {@link #consume}, but
|
||||||
|
* performs the consumption in the background and notifies completion through
|
||||||
|
* the provided listener. This method is safe to call from a UI thread.
|
||||||
|
*
|
||||||
|
* @param purchase The purchase to be consumed.
|
||||||
|
* @param listener The listener to notify when the consumption operation finishes.
|
||||||
|
*/
|
||||||
|
public void consumeAsync(Purchase purchase, OnConsumeFinishedListener listener) {
|
||||||
|
checkNotDisposed();
|
||||||
|
checkSetupDone("consume");
|
||||||
|
List<Purchase> purchases = new ArrayList<Purchase>();
|
||||||
|
purchases.add(purchase);
|
||||||
|
consumeAsyncInternal(purchases, listener, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Same as {@link consumeAsync}, but for multiple items at once.
|
||||||
|
*
|
||||||
|
* @param purchases The list of PurchaseInfo objects representing the purchases to consume.
|
||||||
|
* @param listener The listener to notify when the consumption operation finishes.
|
||||||
|
*/
|
||||||
|
public void consumeAsync(List<Purchase> purchases, OnConsumeMultiFinishedListener listener) {
|
||||||
|
checkNotDisposed();
|
||||||
|
checkSetupDone("consume");
|
||||||
|
consumeAsyncInternal(purchases, null, listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a human-readable description for the given response code.
|
||||||
|
*
|
||||||
|
* @param code The response code
|
||||||
|
* @return A human-readable string explaining the result code.
|
||||||
|
* It also includes the result code numerically.
|
||||||
|
*/
|
||||||
|
public static String getResponseDesc(int code) {
|
||||||
|
String[] iab_msgs = ("0:OK/1:User Canceled/2:Unknown/" +
|
||||||
|
"3:Billing Unavailable/4:Item unavailable/" +
|
||||||
|
"5:Developer Error/6:Error/7:Item Already Owned/" +
|
||||||
|
"8:Item not owned").split("/");
|
||||||
|
String[] iabhelper_msgs = ("0:OK/-1001:Remote exception during initialization/" +
|
||||||
|
"-1002:Bad response received/" +
|
||||||
|
"-1003:Purchase signature verification failed/" +
|
||||||
|
"-1004:Send intent failed/" +
|
||||||
|
"-1005:User cancelled/" +
|
||||||
|
"-1006:Unknown purchase response/" +
|
||||||
|
"-1007:Missing token/" +
|
||||||
|
"-1008:Unknown error/" +
|
||||||
|
"-1009:Subscriptions not available/" +
|
||||||
|
"-1010:Invalid consumption attempt").split("/");
|
||||||
|
|
||||||
|
if (code <= IABHELPER_ERROR_BASE) {
|
||||||
|
int index = IABHELPER_ERROR_BASE - code;
|
||||||
|
if (index >= 0 && index < iabhelper_msgs.length) return iabhelper_msgs[index];
|
||||||
|
else return String.valueOf(code) + ":Unknown IAB Helper Error";
|
||||||
|
} else if (code < 0 || code >= iab_msgs.length)
|
||||||
|
return String.valueOf(code) + ":Unknown";
|
||||||
|
else
|
||||||
|
return iab_msgs[code];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Checks that setup was done; if not, throws an exception.
|
||||||
|
void checkSetupDone(String operation) {
|
||||||
|
if (!mSetupDone) {
|
||||||
|
logError("Illegal state for operation (" + operation + "): IAB helper is not set up.");
|
||||||
|
throw new IllegalStateException("IAB helper is not set up. Can't perform operation: " + operation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Workaround to bug where sometimes response codes come as Long instead of Integer
|
||||||
|
int getResponseCodeFromBundle(Bundle b) {
|
||||||
|
Object o = b.get(RESPONSE_CODE);
|
||||||
|
if (o == null) {
|
||||||
|
logDebug("Bundle with null response code, assuming OK (known issue)");
|
||||||
|
return BILLING_RESPONSE_RESULT_OK;
|
||||||
|
} else if (o instanceof Integer) return ((Integer) o).intValue();
|
||||||
|
else if (o instanceof Long) return (int) ((Long) o).longValue();
|
||||||
|
else {
|
||||||
|
logError("Unexpected type for bundle response code.");
|
||||||
|
logError(o.getClass().getName());
|
||||||
|
throw new RuntimeException("Unexpected type for bundle response code: " + o.getClass().getName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Workaround to bug where sometimes response codes come as Long instead of Integer
|
||||||
|
int getResponseCodeFromIntent(Intent i) {
|
||||||
|
Object o = i.getExtras().get(RESPONSE_CODE);
|
||||||
|
if (o == null) {
|
||||||
|
logError("Intent with no response code, assuming OK (known issue)");
|
||||||
|
return BILLING_RESPONSE_RESULT_OK;
|
||||||
|
} else if (o instanceof Integer) return ((Integer) o).intValue();
|
||||||
|
else if (o instanceof Long) return (int) ((Long) o).longValue();
|
||||||
|
else {
|
||||||
|
logError("Unexpected type for intent response code.");
|
||||||
|
logError(o.getClass().getName());
|
||||||
|
throw new RuntimeException("Unexpected type for intent response code: " + o.getClass().getName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void flagStartAsync(String operation) {
|
||||||
|
if (mAsyncInProgress) throw new IllegalStateException("Can't start async operation (" +
|
||||||
|
operation + ") because another async operation(" + mAsyncOperation + ") is in progress.");
|
||||||
|
mAsyncOperation = operation;
|
||||||
|
mAsyncInProgress = true;
|
||||||
|
logDebug("Starting async operation: " + operation);
|
||||||
|
}
|
||||||
|
|
||||||
|
void flagEndAsync() {
|
||||||
|
logDebug("Ending async operation: " + mAsyncOperation);
|
||||||
|
mAsyncOperation = "";
|
||||||
|
mAsyncInProgress = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
int queryPurchases(Inventory inv, String itemType) throws JSONException, RemoteException {
|
||||||
|
// Query purchases
|
||||||
|
logDebug("Querying owned items, item type: " + itemType);
|
||||||
|
logDebug("Package name: " + mContext.getPackageName());
|
||||||
|
boolean verificationFailed = false;
|
||||||
|
String continueToken = null;
|
||||||
|
|
||||||
|
do {
|
||||||
|
logDebug("Calling getPurchases with continuation token: " + continueToken);
|
||||||
|
Bundle ownedItems = mService.getPurchases(3, mContext.getPackageName(),
|
||||||
|
itemType, continueToken);
|
||||||
|
|
||||||
|
int response = getResponseCodeFromBundle(ownedItems);
|
||||||
|
logDebug("Owned items response: " + String.valueOf(response));
|
||||||
|
if (response != BILLING_RESPONSE_RESULT_OK) {
|
||||||
|
logDebug("getPurchases() failed: " + getResponseDesc(response));
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
if (!ownedItems.containsKey(RESPONSE_INAPP_ITEM_LIST)
|
||||||
|
|| !ownedItems.containsKey(RESPONSE_INAPP_PURCHASE_DATA_LIST)
|
||||||
|
|| !ownedItems.containsKey(RESPONSE_INAPP_SIGNATURE_LIST)) {
|
||||||
|
logError("Bundle returned from getPurchases() doesn't contain required fields.");
|
||||||
|
return IABHELPER_BAD_RESPONSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
ArrayList<String> ownedSkus = ownedItems.getStringArrayList(
|
||||||
|
RESPONSE_INAPP_ITEM_LIST);
|
||||||
|
ArrayList<String> purchaseDataList = ownedItems.getStringArrayList(
|
||||||
|
RESPONSE_INAPP_PURCHASE_DATA_LIST);
|
||||||
|
ArrayList<String> signatureList = ownedItems.getStringArrayList(
|
||||||
|
RESPONSE_INAPP_SIGNATURE_LIST);
|
||||||
|
|
||||||
|
for (int i = 0; i < purchaseDataList.size(); ++i) {
|
||||||
|
String purchaseData = purchaseDataList.get(i);
|
||||||
|
String signature = signatureList.get(i);
|
||||||
|
String sku = ownedSkus.get(i);
|
||||||
|
if (Security.verifyPurchase(mSignatureBase64, purchaseData, signature)) {
|
||||||
|
logDebug("Sku is owned: " + sku);
|
||||||
|
Purchase purchase = new Purchase(itemType, purchaseData, signature);
|
||||||
|
|
||||||
|
if (TextUtils.isEmpty(purchase.getToken())) {
|
||||||
|
logWarn("BUG: empty/null token!");
|
||||||
|
logDebug("Purchase data: " + purchaseData);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Record ownership and token
|
||||||
|
inv.addPurchase(purchase);
|
||||||
|
} else {
|
||||||
|
logWarn("Purchase signature verification **FAILED**. Not adding item.");
|
||||||
|
logDebug(" Purchase data: " + purchaseData);
|
||||||
|
logDebug(" Signature: " + signature);
|
||||||
|
verificationFailed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
continueToken = ownedItems.getString(INAPP_CONTINUATION_TOKEN);
|
||||||
|
logDebug("Continuation token: " + continueToken);
|
||||||
|
} while (!TextUtils.isEmpty(continueToken));
|
||||||
|
|
||||||
|
return verificationFailed ? IABHELPER_VERIFICATION_FAILED : BILLING_RESPONSE_RESULT_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
int querySkuDetails(String itemType, Inventory inv, List<String> moreSkus)
|
||||||
|
throws RemoteException, JSONException {
|
||||||
|
logDebug("Querying SKU details.");
|
||||||
|
ArrayList<String> skuList = new ArrayList<String>();
|
||||||
|
skuList.addAll(inv.getAllOwnedSkus(itemType));
|
||||||
|
if (moreSkus != null) {
|
||||||
|
for (String sku : moreSkus) {
|
||||||
|
if (!skuList.contains(sku)) {
|
||||||
|
skuList.add(sku);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (skuList.size() == 0) {
|
||||||
|
logDebug("queryPrices: nothing to do because there are no SKUs.");
|
||||||
|
return BILLING_RESPONSE_RESULT_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
Bundle querySkus = new Bundle();
|
||||||
|
querySkus.putStringArrayList(GET_SKU_DETAILS_ITEM_LIST, skuList);
|
||||||
|
Bundle skuDetails = mService.getSkuDetails(3, mContext.getPackageName(),
|
||||||
|
itemType, querySkus);
|
||||||
|
|
||||||
|
if (!skuDetails.containsKey(RESPONSE_GET_SKU_DETAILS_LIST)) {
|
||||||
|
int response = getResponseCodeFromBundle(skuDetails);
|
||||||
|
if (response != BILLING_RESPONSE_RESULT_OK) {
|
||||||
|
logDebug("getSkuDetails() failed: " + getResponseDesc(response));
|
||||||
|
return response;
|
||||||
|
} else {
|
||||||
|
logError("getSkuDetails() returned a bundle with neither an error nor a detail list.");
|
||||||
|
return IABHELPER_BAD_RESPONSE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ArrayList<String> responseList = skuDetails.getStringArrayList(
|
||||||
|
RESPONSE_GET_SKU_DETAILS_LIST);
|
||||||
|
|
||||||
|
for (String thisResponse : responseList) {
|
||||||
|
SkuDetails d = new SkuDetails(itemType, thisResponse);
|
||||||
|
logDebug("Got sku details: " + d);
|
||||||
|
inv.addSkuDetails(d);
|
||||||
|
}
|
||||||
|
return BILLING_RESPONSE_RESULT_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void consumeAsyncInternal(final List<Purchase> purchases,
|
||||||
|
final OnConsumeFinishedListener singleListener,
|
||||||
|
final OnConsumeMultiFinishedListener multiListener) {
|
||||||
|
final Handler handler = new Handler();
|
||||||
|
flagStartAsync("consume");
|
||||||
|
(new Thread(new Runnable() {
|
||||||
|
public void run() {
|
||||||
|
final List<IabResult> results = new ArrayList<IabResult>();
|
||||||
|
for (Purchase purchase : purchases) {
|
||||||
|
try {
|
||||||
|
consume(purchase);
|
||||||
|
results.add(new IabResult(BILLING_RESPONSE_RESULT_OK, "Successful consume of sku " + purchase.getSku()));
|
||||||
|
} catch (IabException ex) {
|
||||||
|
results.add(ex.getResult());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
flagEndAsync();
|
||||||
|
if (!mDisposed && singleListener != null) {
|
||||||
|
handler.post(new Runnable() {
|
||||||
|
public void run() {
|
||||||
|
singleListener.onConsumeFinished(purchases.get(0), results.get(0));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (!mDisposed && multiListener != null) {
|
||||||
|
handler.post(new Runnable() {
|
||||||
|
public void run() {
|
||||||
|
multiListener.onConsumeMultiFinished(purchases, results);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})).start();
|
||||||
|
}
|
||||||
|
|
||||||
|
void logDebug(String msg) {
|
||||||
|
if (mDebugLog) Log.d(mDebugTag, msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
void logError(String msg) {
|
||||||
|
Log.e(mDebugTag, "In-app billing error: " + msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
void logWarn(String msg) {
|
||||||
|
Log.w(mDebugTag, "In-app billing warning: " + msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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(); }
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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<String,SkuDetails> mSkuMap = new HashMap<String,SkuDetails>();
|
||||||
|
Map<String,Purchase> mPurchaseMap = new HashMap<String,Purchase>();
|
||||||
|
|
||||||
|
Inventory() { }
|
||||||
|
|
||||||
|
/** Returns the listing details for an in-app product. */
|
||||||
|
public SkuDetails getSkuDetails(String sku) {
|
||||||
|
return mSkuMap.get(sku);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns purchase information for a given product, or null if there is no purchase. */
|
||||||
|
public Purchase getPurchase(String sku) {
|
||||||
|
return mPurchaseMap.get(sku);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns whether or not there exists a purchase of the given product. */
|
||||||
|
public boolean hasPurchase(String sku) {
|
||||||
|
return mPurchaseMap.containsKey(sku);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Return whether or not details about the given product are available. */
|
||||||
|
public boolean hasDetails(String sku) {
|
||||||
|
return mSkuMap.containsKey(sku);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Erase a purchase (locally) from the inventory, given its product ID. This just
|
||||||
|
* modifies the Inventory object locally and has no effect on the server! This is
|
||||||
|
* useful when you have an existing Inventory object which you know to be up to date,
|
||||||
|
* and you have just consumed an item successfully, which means that erasing its
|
||||||
|
* purchase data from the Inventory you already have is quicker than querying for
|
||||||
|
* a new Inventory.
|
||||||
|
*/
|
||||||
|
public void erasePurchase(String sku) {
|
||||||
|
if (mPurchaseMap.containsKey(sku)) mPurchaseMap.remove(sku);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns a list of all owned product IDs. */
|
||||||
|
List<String> getAllOwnedSkus() {
|
||||||
|
return new ArrayList<String>(mPurchaseMap.keySet());
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns a list of all owned product IDs of a given type */
|
||||||
|
List<String> getAllOwnedSkus(String itemType) {
|
||||||
|
List<String> result = new ArrayList<String>();
|
||||||
|
for (Purchase p : mPurchaseMap.values()) {
|
||||||
|
if (p.getItemType().equals(itemType)) result.add(p.getSku());
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns a list of all purchases. */
|
||||||
|
List<Purchase> getAllPurchases() {
|
||||||
|
return new ArrayList<Purchase>(mPurchaseMap.values());
|
||||||
|
}
|
||||||
|
|
||||||
|
void addSkuDetails(SkuDetails d) {
|
||||||
|
mSkuMap.put(d.getSku(), d);
|
||||||
|
}
|
||||||
|
|
||||||
|
void addPurchase(Purchase p) {
|
||||||
|
mPurchaseMap.put(p.getSku(), p);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<String> mFilterTags = new ArrayList<>();
|
||||||
|
public int mPID = INVALID_PID;
|
||||||
|
|
||||||
|
public boolean mUseLogcatFileOut = false;
|
||||||
|
|
||||||
|
private LogDumper mLogDumper = null;
|
||||||
|
|
||||||
|
public static final int EVENT_RESTART_LOG = 1001;
|
||||||
|
|
||||||
|
private RestartHandler mHandler;
|
||||||
|
|
||||||
|
private static class RestartHandler extends Handler {
|
||||||
|
final LogRecorder logRecorder;
|
||||||
|
public RestartHandler(LogRecorder logRecorder) {
|
||||||
|
this.logRecorder = logRecorder;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void handleMessage(Message msg) {
|
||||||
|
if (msg.what == EVENT_RESTART_LOG) {
|
||||||
|
logRecorder.stop();
|
||||||
|
logRecorder.start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public LogRecorder() {
|
||||||
|
mHandler = new RestartHandler(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void start() {
|
||||||
|
// make sure the out folder exist
|
||||||
|
// TODO support multi-phase path
|
||||||
|
File file = new File(mFolderPath);
|
||||||
|
if (!file.exists()) {
|
||||||
|
file.mkdirs();
|
||||||
|
}
|
||||||
|
|
||||||
|
String cmdStr = collectLogcatCommand();
|
||||||
|
|
||||||
|
if (mLogDumper != null) {
|
||||||
|
mLogDumper.stopDumping();
|
||||||
|
mLogDumper = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
mLogDumper = new LogDumper(mFolderPath, mFileSuffix, mFileSizeLimitation, cmdStr, mHandler);
|
||||||
|
mLogDumper.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void stop() {
|
||||||
|
// TODO maybe should clean the log buffer first?
|
||||||
|
if (mLogDumper != null) {
|
||||||
|
mLogDumper.stopDumping();
|
||||||
|
mLogDumper = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String collectLogcatCommand() {
|
||||||
|
StringBuilder stringBuilder = new StringBuilder();
|
||||||
|
final String SPACE = " ";
|
||||||
|
stringBuilder.append("logcat");
|
||||||
|
|
||||||
|
// TODO select ring buffer, -b
|
||||||
|
|
||||||
|
// TODO set out format
|
||||||
|
stringBuilder.append(SPACE);
|
||||||
|
stringBuilder.append("-v time");
|
||||||
|
|
||||||
|
// append tag filters
|
||||||
|
String levelStr = getLevelStr();
|
||||||
|
|
||||||
|
if (!mFilterTags.isEmpty()) {
|
||||||
|
stringBuilder.append(SPACE);
|
||||||
|
stringBuilder.append("-s");
|
||||||
|
for (int i = 0; i < mFilterTags.size(); i++) {
|
||||||
|
String tag = mFilterTags.get(i) + ":" + levelStr;
|
||||||
|
stringBuilder.append(SPACE);
|
||||||
|
stringBuilder.append(tag);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (!TextUtils.isEmpty(levelStr)) {
|
||||||
|
stringBuilder.append(SPACE);
|
||||||
|
stringBuilder.append("*:" + levelStr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// logcat -f , but the rotated count default is 4?
|
||||||
|
// can`t be sure to use that feature
|
||||||
|
if (mPID != INVALID_PID) {
|
||||||
|
mUseLogcatFileOut = false;
|
||||||
|
String pidStr = adjustPIDStr();
|
||||||
|
if (!TextUtils.isEmpty(pidStr)) {
|
||||||
|
stringBuilder.append(SPACE);
|
||||||
|
stringBuilder.append("|");
|
||||||
|
stringBuilder.append(SPACE);
|
||||||
|
stringBuilder.append("grep (" + pidStr + ")");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return stringBuilder.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getLevelStr() {
|
||||||
|
switch (mLevel) {
|
||||||
|
case 2:
|
||||||
|
return "V";
|
||||||
|
case 3:
|
||||||
|
return "D";
|
||||||
|
case 4:
|
||||||
|
return "I";
|
||||||
|
case 5:
|
||||||
|
return "W";
|
||||||
|
case 6:
|
||||||
|
return "E";
|
||||||
|
case 7:
|
||||||
|
return "F";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "V";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Android`s user app pid is bigger than 1000.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
private String adjustPIDStr() {
|
||||||
|
if (mPID == INVALID_PID) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
String pidStr = String.valueOf(mPID);
|
||||||
|
int length = pidStr.length();
|
||||||
|
if (length < 4) {
|
||||||
|
pidStr = " 0" + pidStr;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (length == 4) {
|
||||||
|
pidStr = " " + pidStr;
|
||||||
|
}
|
||||||
|
|
||||||
|
return pidStr;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private class LogDumper extends Thread {
|
||||||
|
final String logPath;
|
||||||
|
final String logFileSuffix;
|
||||||
|
final int logFileLimitation;
|
||||||
|
final String logCmd;
|
||||||
|
|
||||||
|
final RestartHandler restartHandler;
|
||||||
|
|
||||||
|
private Process logcatProc;
|
||||||
|
private BufferedReader mReader = null;
|
||||||
|
private FileOutputStream out = null;
|
||||||
|
|
||||||
|
private boolean mRunning = true;
|
||||||
|
final private Object mRunningLock = new Object();
|
||||||
|
|
||||||
|
private long currentFileSize;
|
||||||
|
|
||||||
|
public LogDumper(String folderPath, String suffix,
|
||||||
|
int fileSizeLimitation, String command,
|
||||||
|
RestartHandler handler) {
|
||||||
|
logPath = folderPath;
|
||||||
|
logFileSuffix = suffix;
|
||||||
|
logFileLimitation = fileSizeLimitation;
|
||||||
|
logCmd = command;
|
||||||
|
restartHandler = handler;
|
||||||
|
|
||||||
|
String date = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss")
|
||||||
|
.format(new Date(System.currentTimeMillis()));
|
||||||
|
String fileName = (TextUtils.isEmpty(logFileSuffix)) ? date : (logFileSuffix + "-"+ date);
|
||||||
|
try {
|
||||||
|
out = new FileOutputStream(new File(logPath, fileName + ".log"));
|
||||||
|
} catch (FileNotFoundException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void stopDumping() {
|
||||||
|
synchronized (mRunningLock) {
|
||||||
|
mRunning = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
try {
|
||||||
|
logcatProc = Runtime.getRuntime().exec(logCmd);
|
||||||
|
mReader = new BufferedReader(new InputStreamReader(
|
||||||
|
logcatProc.getInputStream()), 1024);
|
||||||
|
String line = null;
|
||||||
|
while (mRunning && (line = mReader.readLine()) != null) {
|
||||||
|
if (!mRunning) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (line.length() == 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (out != null && !line.isEmpty()) {
|
||||||
|
byte[] data = (line + "\n").getBytes();
|
||||||
|
out.write(data);
|
||||||
|
if (logFileLimitation != 0) {
|
||||||
|
currentFileSize += data.length;
|
||||||
|
if (currentFileSize > logFileLimitation*1024) {
|
||||||
|
restartHandler.sendEmptyMessage(EVENT_RESTART_LOG);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
} finally {
|
||||||
|
if (logcatProc != null) {
|
||||||
|
logcatProc.destroy();
|
||||||
|
logcatProc = null;
|
||||||
|
}
|
||||||
|
if (mReader != null) {
|
||||||
|
try {
|
||||||
|
mReader.close();
|
||||||
|
mReader = null;
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (out != null) {
|
||||||
|
try {
|
||||||
|
out.close();
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
out = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Builder {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* context object
|
||||||
|
*/
|
||||||
|
private Context mContext;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* the folder name that we save log files to,
|
||||||
|
* just folder name, not the whole path,
|
||||||
|
* if set this, will save log files to /sdcard/$mLogFolderName folder,
|
||||||
|
* use /sdcard/$ApplicationName as default.
|
||||||
|
*/
|
||||||
|
private String mLogFolderName;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* the whole folder path that we save log files to,
|
||||||
|
* this setting`s priority is bigger than folder name.
|
||||||
|
*/
|
||||||
|
private String mLogFolderPath;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* the log file suffix,
|
||||||
|
* if this is sot, it will be appended to log file name automatically
|
||||||
|
*/
|
||||||
|
private String mLogFileNameSuffix = "";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* single log file size limitation,
|
||||||
|
* in k-bytes, ex. set to 16, is 16KB limitation.
|
||||||
|
*/
|
||||||
|
private int mLogFileSizeLimitation = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* log level, see android.util.Log, 2 - 7,
|
||||||
|
* if not be set, will use verbose as default
|
||||||
|
*/
|
||||||
|
private int mLogLevel = LogRecorder.LOG_LEVEL_NO_SET;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* can set several filter tags
|
||||||
|
* logcat -s ActivityManager:V SystemUI:V
|
||||||
|
*/
|
||||||
|
private List<String> mLogFilterTags = new ArrayList<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* filter through pid, by setting this with your APP PID,
|
||||||
|
* the log recorder will just record the APP`s own log,
|
||||||
|
* use one call: android.os.Process.myPid().
|
||||||
|
*/
|
||||||
|
private int mPID = LogRecorder.INVALID_PID;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* which log buffer to catch...
|
||||||
|
* <p/>
|
||||||
|
* Request alternate ring buffer, 'main', 'system', 'radio'
|
||||||
|
* or 'events'. Multiple -b parameters are allowed and the
|
||||||
|
* results are interleaved.
|
||||||
|
* <p/>
|
||||||
|
* The default is -b main -b system.
|
||||||
|
*/
|
||||||
|
private int mLogBuffersSelected = LogRecorder.LOG_BUFFER_DEFAULT;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* log output format, don`t support config yet, use $time format as default.
|
||||||
|
* <p/>
|
||||||
|
* Log messages contain a number of metadata fields, in addition to the tag and priority.
|
||||||
|
* You can modify the output format for messages so that they display a specific metadata
|
||||||
|
* field. To do so, you use the -v option and specify one of the supported output formats
|
||||||
|
* listed below.
|
||||||
|
* <p/>
|
||||||
|
* brief — Display priority/tag and PID of the process issuing the message.
|
||||||
|
* process — Display PID only.
|
||||||
|
* tag — Display the priority/tag only.
|
||||||
|
* thread - Display the priority, tag, and the PID(process ID) and TID(thread ID)
|
||||||
|
* of the thread issuing the message.
|
||||||
|
* raw — Display the raw log message, with no other metadata fields.
|
||||||
|
* time — Display the date, invocation time, priority/tag, and PID of
|
||||||
|
* the process issuing the message.
|
||||||
|
* threadtime — Display the date, invocation time, priority, tag, and the PID(process ID)
|
||||||
|
* and TID(thread ID) of the thread issuing the message.
|
||||||
|
* long — Display all metadata fields and separate messages with blank lines.
|
||||||
|
*/
|
||||||
|
private int mLogOutFormat;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* set log out folder name
|
||||||
|
*
|
||||||
|
* @param logFolderName folder name
|
||||||
|
* @return The same Builder.
|
||||||
|
*/
|
||||||
|
public Builder setLogFolderName(String logFolderName) {
|
||||||
|
this.mLogFolderName = logFolderName;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* set log out folder path
|
||||||
|
*
|
||||||
|
* @param logFolderPath out folder absolute path
|
||||||
|
* @return the same Builder
|
||||||
|
*/
|
||||||
|
public Builder setLogFolderPath(String logFolderPath) {
|
||||||
|
this.mLogFolderPath = logFolderPath;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* set log file name suffix
|
||||||
|
*
|
||||||
|
* @param logFileNameSuffix auto appened suffix
|
||||||
|
* @return the same Builder
|
||||||
|
*/
|
||||||
|
public Builder setLogFileNameSuffix(String logFileNameSuffix) {
|
||||||
|
this.mLogFileNameSuffix = logFileNameSuffix;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* set the file size limitation
|
||||||
|
*
|
||||||
|
* @param fileSizeLimitation file size limitation in KB
|
||||||
|
* @return the same Builder
|
||||||
|
*/
|
||||||
|
public Builder setLogFileSizeLimitation(int fileSizeLimitation) {
|
||||||
|
this.mLogFileSizeLimitation = fileSizeLimitation;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* set the log level
|
||||||
|
*
|
||||||
|
* @param logLevel log level, 2-7
|
||||||
|
* @return the same Builder
|
||||||
|
*/
|
||||||
|
public Builder setLogLevel(int logLevel) {
|
||||||
|
this.mLogLevel = logLevel;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* add log filterspec tag name, can add multiple ones,
|
||||||
|
* they use the same log level set by setLogLevel()
|
||||||
|
*
|
||||||
|
* @param tag tag name
|
||||||
|
* @return the same Builder
|
||||||
|
*/
|
||||||
|
public Builder addLogFilterTag(String tag) {
|
||||||
|
mLogFilterTags.add(tag);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* which process`s log
|
||||||
|
*
|
||||||
|
* @param mPID process id
|
||||||
|
* @return the same Builder
|
||||||
|
*/
|
||||||
|
public Builder setPID(int mPID) {
|
||||||
|
this.mPID = mPID;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* -b radio, -b main, -b system, -b events
|
||||||
|
* -b main -b system as default
|
||||||
|
*
|
||||||
|
* @param logBuffersSelected one of
|
||||||
|
* LOG_BUFFER_MAIN = 1 << 0;
|
||||||
|
* LOG_BUFFER_SYSTEM = 1 << 1;
|
||||||
|
* LOG_BUFFER_RADIO = 1 << 2;
|
||||||
|
* LOG_BUFFER_EVENTS = 1 << 3;
|
||||||
|
* LOG_BUFFER_KERNEL = 1 << 4;
|
||||||
|
* @return the same Builder
|
||||||
|
*/
|
||||||
|
public Builder setLogBufferSelected(int logBuffersSelected) {
|
||||||
|
this.mLogBuffersSelected = logBuffersSelected;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* sets log out format, -v parameter
|
||||||
|
*
|
||||||
|
* @param logOutFormat out format, like -v time
|
||||||
|
* @return the same Builder
|
||||||
|
*/
|
||||||
|
public Builder setLogOutFormat(int logOutFormat) {
|
||||||
|
this.mLogOutFormat = mLogOutFormat;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder(Context context) {
|
||||||
|
mContext = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* call this only if mLogFolderName and mLogFolderPath not
|
||||||
|
* be set both.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
private void applyAppNameAsOutfolderName() {
|
||||||
|
try {
|
||||||
|
String appName = mContext.getPackageName();
|
||||||
|
String versionName = mContext.getPackageManager().getPackageInfo(
|
||||||
|
appName, 0).versionName;
|
||||||
|
int versionCode = mContext.getPackageManager()
|
||||||
|
.getPackageInfo(appName, 0).versionCode;
|
||||||
|
mLogFolderName = appName + "-" + versionName + "-" + versionCode;
|
||||||
|
mLogFolderPath = applyOutfolderPath();
|
||||||
|
} catch (Exception e) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String applyOutfolderPath() {
|
||||||
|
String outPath = "";
|
||||||
|
if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
|
||||||
|
outPath = Environment.getExternalStorageDirectory()
|
||||||
|
.getAbsolutePath() + File.separator + mLogFolderName;
|
||||||
|
}
|
||||||
|
|
||||||
|
return outPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Combine all of the options that have been set and return
|
||||||
|
* a new {@link LogRecorder} object.
|
||||||
|
*/
|
||||||
|
public LogRecorder build() {
|
||||||
|
LogRecorder logRecorder = new LogRecorder();
|
||||||
|
|
||||||
|
// no folder name & folder path be set
|
||||||
|
if (TextUtils.isEmpty(mLogFolderName)
|
||||||
|
&& TextUtils.isEmpty(mLogFolderPath)) {
|
||||||
|
applyAppNameAsOutfolderName();
|
||||||
|
}
|
||||||
|
|
||||||
|
// make sure out path be set
|
||||||
|
if (TextUtils.isEmpty(mLogFolderPath)) {
|
||||||
|
mLogFolderPath = applyOutfolderPath();
|
||||||
|
}
|
||||||
|
|
||||||
|
logRecorder.mFolderPath = mLogFolderPath;
|
||||||
|
logRecorder.mFileSuffix = mLogFileNameSuffix;
|
||||||
|
logRecorder.mFileSizeLimitation = mLogFileSizeLimitation;
|
||||||
|
logRecorder.mLevel = mLogLevel;
|
||||||
|
if (!mLogFilterTags.isEmpty()) {
|
||||||
|
for (int i = 0; i < mLogFilterTags.size(); i++) {
|
||||||
|
logRecorder.mFilterTags.add(mLogFilterTags.get(i));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
logRecorder.mPID = mPID;
|
||||||
|
|
||||||
|
return logRecorder;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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; }
|
||||||
|
}
|
||||||
@@ -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<DecodeHintType, Object> HINTS = new EnumMap<>(DecodeHintType.class);
|
||||||
|
|
||||||
|
static {
|
||||||
|
List<BarcodeFormat> allFormats = new ArrayList<>();
|
||||||
|
allFormats.add(BarcodeFormat.AZTEC);
|
||||||
|
allFormats.add(BarcodeFormat.CODABAR);
|
||||||
|
allFormats.add(BarcodeFormat.CODE_39);
|
||||||
|
allFormats.add(BarcodeFormat.CODE_93);
|
||||||
|
allFormats.add(BarcodeFormat.CODE_128);
|
||||||
|
allFormats.add(BarcodeFormat.DATA_MATRIX);
|
||||||
|
allFormats.add(BarcodeFormat.EAN_8);
|
||||||
|
allFormats.add(BarcodeFormat.EAN_13);
|
||||||
|
allFormats.add(BarcodeFormat.ITF);
|
||||||
|
allFormats.add(BarcodeFormat.MAXICODE);
|
||||||
|
allFormats.add(BarcodeFormat.PDF_417);
|
||||||
|
allFormats.add(BarcodeFormat.QR_CODE);
|
||||||
|
allFormats.add(BarcodeFormat.RSS_14);
|
||||||
|
allFormats.add(BarcodeFormat.RSS_EXPANDED);
|
||||||
|
allFormats.add(BarcodeFormat.UPC_A);
|
||||||
|
allFormats.add(BarcodeFormat.UPC_E);
|
||||||
|
allFormats.add(BarcodeFormat.UPC_EAN_EXTENSION);
|
||||||
|
HINTS.put(DecodeHintType.TRY_HARDER, BarcodeFormat.QR_CODE);
|
||||||
|
HINTS.put(DecodeHintType.POSSIBLE_FORMATS, allFormats);
|
||||||
|
HINTS.put(DecodeHintType.CHARACTER_SET, "utf-8");
|
||||||
|
}
|
||||||
|
|
||||||
|
private QRCodeDecoder() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 同步解析本地图片二维码。该方法是耗时操作,请在子线程中调用。
|
||||||
|
*
|
||||||
|
* @param picturePath 要解析的二维码图片本地路径
|
||||||
|
* @return 返回二维码图片里的内容 或 null
|
||||||
|
*/
|
||||||
|
public static String syncDecodeQRCode(String picturePath) {
|
||||||
|
return syncDecodeQRCode(getDecodeAbleBitmap(picturePath));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 同步解析bitmap二维码。该方法是耗时操作,请在子线程中调用。
|
||||||
|
*
|
||||||
|
* @param bitmap 要解析的二维码图片
|
||||||
|
* @return 返回二维码图片里的内容 或 null
|
||||||
|
*/
|
||||||
|
public static String syncDecodeQRCode(Bitmap bitmap) {
|
||||||
|
Result result = null;
|
||||||
|
RGBLuminanceSource source = null;
|
||||||
|
try {
|
||||||
|
int width = bitmap.getWidth();
|
||||||
|
int height = bitmap.getHeight();
|
||||||
|
int[] pixels = new int[width * height];
|
||||||
|
bitmap.getPixels(pixels, 0, width, 0, 0, width, height);
|
||||||
|
source = new RGBLuminanceSource(width, height, pixels);
|
||||||
|
result = new MultiFormatReader().decode(new BinaryBitmap(new HybridBinarizer(source)), HINTS);
|
||||||
|
return result.getText();
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
if (source != null) {
|
||||||
|
try {
|
||||||
|
result = new MultiFormatReader().decode(new BinaryBitmap(new GlobalHistogramBinarizer(source)), HINTS);
|
||||||
|
return result.getText();
|
||||||
|
} catch (Throwable e2) {
|
||||||
|
e2.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将本地图片文件转换成可解码二维码的 Bitmap。为了避免图片太大,这里对图片进行了压缩。感谢 https://github.com/devilsen 提的 PR
|
||||||
|
*
|
||||||
|
* @param picturePath 本地图片文件路径
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
private static Bitmap getDecodeAbleBitmap(String picturePath) {
|
||||||
|
try {
|
||||||
|
BitmapFactory.Options options = new BitmapFactory.Options();
|
||||||
|
options.inJustDecodeBounds = true;
|
||||||
|
BitmapFactory.decodeFile(picturePath, options);
|
||||||
|
int sampleSize = options.outHeight / 400;
|
||||||
|
if (sampleSize <= 0) {
|
||||||
|
sampleSize = 1;
|
||||||
|
}
|
||||||
|
options.inSampleSize = sampleSize;
|
||||||
|
options.inJustDecodeBounds = false;
|
||||||
|
return BitmapFactory.decodeFile(picturePath, options);
|
||||||
|
} catch (Exception e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,52 +1,31 @@
|
|||||||
package com.v2ray.ang
|
package com.v2ray.ang
|
||||||
|
|
||||||
import android.content.Context
|
import android.app.Application
|
||||||
import android.content.pm.PackageManager
|
//import com.squareup.leakcanary.LeakCanary
|
||||||
import android.os.Build
|
import com.v2ray.ang.util.AngConfigManager
|
||||||
import androidx.multidex.MultiDexApplication
|
import me.dozen.dpreference.DPreference
|
||||||
import androidx.work.Configuration
|
import org.jetbrains.anko.defaultSharedPreferences
|
||||||
import androidx.work.WorkManager
|
|
||||||
import com.tencent.mmkv.MMKV
|
|
||||||
import com.v2ray.ang.handler.SettingsManager
|
|
||||||
import com.v2ray.ang.util.Utils
|
|
||||||
|
|
||||||
class AngApplication : MultiDexApplication() {
|
class AngApplication : Application() {
|
||||||
companion object {
|
companion object {
|
||||||
//const val PREF_LAST_VERSION = "pref_last_version"
|
const val PREF_LAST_VERSION = "pref_last_version"
|
||||||
lateinit var application: AngApplication
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun attachBaseContext(base: Context?) {
|
var firstRun = false
|
||||||
super.attachBaseContext(base)
|
private set
|
||||||
application = this
|
|
||||||
}
|
|
||||||
|
|
||||||
private val workManagerConfiguration: Configuration = Configuration.Builder()
|
val defaultDPreference by lazy { DPreference(this, packageName + "_preferences") }
|
||||||
.setDefaultProcessName("${BuildConfig.APPLICATION_ID}:bg")
|
|
||||||
.build()
|
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
|
|
||||||
// LeakCanary.install(this)
|
// LeakCanary.install(this)
|
||||||
|
|
||||||
// val defaultSharedPreferences = PreferenceManager.getDefaultSharedPreferences(this)
|
firstRun = defaultSharedPreferences.getInt(PREF_LAST_VERSION, 0) != BuildConfig.VERSION_CODE
|
||||||
// firstRun = defaultSharedPreferences.getInt(PREF_LAST_VERSION, 0) != BuildConfig.VERSION_CODE
|
if (firstRun)
|
||||||
// if (firstRun)
|
defaultSharedPreferences.edit().putInt(PREF_LAST_VERSION, BuildConfig.VERSION_CODE).apply()
|
||||||
// defaultSharedPreferences.edit().putInt(PREF_LAST_VERSION, BuildConfig.VERSION_CODE).apply()
|
|
||||||
|
|
||||||
MMKV.initialize(this)
|
//Logger.init().logLevel(if (BuildConfig.DEBUG) LogLevel.FULL else LogLevel.NONE)
|
||||||
|
AngConfigManager.inject(this)
|
||||||
Utils.setNightMode(application)
|
|
||||||
// Initialize WorkManager with the custom configuration
|
|
||||||
WorkManager.initialize(this, workManagerConfiguration)
|
|
||||||
|
|
||||||
SettingsManager.initRoutingRulesets(this)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getPackageInfo(packageName: String) = packageManager.getPackageInfo(
|
|
||||||
packageName, if (Build.VERSION.SDK_INT >= 28) PackageManager.GET_SIGNING_CERTIFICATES
|
|
||||||
else @Suppress("DEPRECATION") PackageManager.GET_SIGNATURES
|
|
||||||
)!!
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,123 +1,45 @@
|
|||||||
package com.v2ray.ang
|
package com.v2ray.ang
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* App Config Const
|
||||||
|
*/
|
||||||
object AppConfig {
|
object AppConfig {
|
||||||
|
const val ANG_PACKAGE = "com.v2ray.ang"
|
||||||
/** The application's package name. */
|
|
||||||
const val ANG_PACKAGE = BuildConfig.APPLICATION_ID
|
|
||||||
|
|
||||||
/** Directory names used in the app's file system. */
|
|
||||||
const val DIR_ASSETS = "assets"
|
|
||||||
const val DIR_BACKUPS = "backups"
|
|
||||||
|
|
||||||
/** Legacy configuration keys. */
|
|
||||||
const val ANG_CONFIG = "ang_config"
|
const val ANG_CONFIG = "ang_config"
|
||||||
|
const val PREF_CURR_CONFIG = "pref_v2ray_config"
|
||||||
|
const val PREF_CURR_CONFIG_GUID = "pref_v2ray_config_guid"
|
||||||
|
const val PREF_CURR_CONFIG_NAME = "pref_v2ray_config_name"
|
||||||
|
const val PREF_CURR_CONFIG_DOMAIN = "pref_v2ray_config_domain"
|
||||||
const val PREF_INAPP_BUY_IS_PREMIUM = "pref_inapp_buy_is_premium"
|
const val PREF_INAPP_BUY_IS_PREMIUM = "pref_inapp_buy_is_premium"
|
||||||
|
const val VMESS_PROTOCOL: String = "vmess://"
|
||||||
/** Preferences mapped to MMKV storage. */
|
const val SS_PROTOCOL: String = "ss://"
|
||||||
const val PREF_SNIFFING_ENABLED = "pref_sniffing_enabled"
|
const val SOCKS_PROTOCOL: String = "socks://"
|
||||||
const val PREF_ROUTE_ONLY_ENABLED = "pref_route_only_enabled"
|
|
||||||
const val PREF_PER_APP_PROXY = "pref_per_app_proxy"
|
|
||||||
const val PREF_PER_APP_PROXY_SET = "pref_per_app_proxy_set"
|
|
||||||
const val PREF_BYPASS_APPS = "pref_bypass_apps"
|
|
||||||
const val PREF_LOCAL_DNS_ENABLED = "pref_local_dns_enabled"
|
|
||||||
const val PREF_FAKE_DNS_ENABLED = "pref_fake_dns_enabled"
|
|
||||||
const val PREF_LOCAL_DNS_PORT = "pref_local_dns_port"
|
|
||||||
const val PREF_VPN_DNS = "pref_vpn_dns"
|
|
||||||
const val PREF_ROUTING_DOMAIN_STRATEGY = "pref_routing_domain_strategy"
|
|
||||||
const val PREF_ROUTING_RULESET = "pref_routing_ruleset"
|
|
||||||
const val PREF_MUX_ENABLED = "pref_mux_enabled"
|
|
||||||
const val PREF_MUX_CONCURRENCY = "pref_mux_concurrency"
|
|
||||||
const val PREF_MUX_XUDP_CONCURRENCY = "pref_mux_xudp_concurrency"
|
|
||||||
const val PREF_MUX_XUDP_QUIC = "pref_mux_xudp_quic"
|
|
||||||
const val PREF_FRAGMENT_ENABLED = "pref_fragment_enabled"
|
|
||||||
const val PREF_FRAGMENT_PACKETS = "pref_fragment_packets"
|
|
||||||
const val PREF_FRAGMENT_LENGTH = "pref_fragment_length"
|
|
||||||
const val PREF_FRAGMENT_INTERVAL = "pref_fragment_interval"
|
|
||||||
const val SUBSCRIPTION_AUTO_UPDATE = "pref_auto_update_subscription"
|
|
||||||
const val SUBSCRIPTION_AUTO_UPDATE_INTERVAL = "pref_auto_update_interval"
|
|
||||||
const val SUBSCRIPTION_DEFAULT_UPDATE_INTERVAL = "1440" // Default is 24 hours
|
|
||||||
const val SUBSCRIPTION_UPDATE_TASK_NAME = "subscription_updater"
|
|
||||||
const val PREF_SPEED_ENABLED = "pref_speed_enabled"
|
|
||||||
const val PREF_CONFIRM_REMOVE = "pref_confirm_remove"
|
|
||||||
const val PREF_START_SCAN_IMMEDIATE = "pref_start_scan_immediate"
|
|
||||||
const val PREF_LANGUAGE = "pref_language"
|
|
||||||
const val PREF_UI_MODE_NIGHT = "pref_ui_mode_night"
|
|
||||||
const val PREF_PREFER_IPV6 = "pref_prefer_ipv6"
|
|
||||||
const val PREF_PROXY_SHARING = "pref_proxy_sharing_enabled"
|
|
||||||
const val PREF_ALLOW_INSECURE = "pref_allow_insecure"
|
|
||||||
const val PREF_SOCKS_PORT = "pref_socks_port"
|
|
||||||
const val PREF_HTTP_PORT = "pref_http_port"
|
|
||||||
const val PREF_REMOTE_DNS = "pref_remote_dns"
|
|
||||||
const val PREF_DOMESTIC_DNS = "pref_domestic_dns"
|
|
||||||
const val PREF_DELAY_TEST_URL = "pref_delay_test_url"
|
|
||||||
const val PREF_LOGLEVEL = "pref_core_loglevel"
|
|
||||||
const val PREF_MODE = "pref_mode"
|
|
||||||
const val PREF_IS_BOOTED = "pref_is_booted"
|
|
||||||
|
|
||||||
/** Cache keys. */
|
|
||||||
const val CACHE_SUBSCRIPTION_ID = "cache_subscription_id"
|
|
||||||
const val CACHE_KEYWORD_FILTER = "cache_keyword_filter"
|
|
||||||
|
|
||||||
/** Protocol identifiers. */
|
|
||||||
const val PROTOCOL_FREEDOM: String = "freedom"
|
|
||||||
|
|
||||||
/** Broadcast actions. */
|
|
||||||
const val BROADCAST_ACTION_SERVICE = "com.v2ray.ang.action.service"
|
const val BROADCAST_ACTION_SERVICE = "com.v2ray.ang.action.service"
|
||||||
const val BROADCAST_ACTION_ACTIVITY = "com.v2ray.ang.action.activity"
|
const val BROADCAST_ACTION_ACTIVITY = "com.v2ray.ang.action.activity"
|
||||||
const val BROADCAST_ACTION_WIDGET_CLICK = "com.v2ray.ang.action.widget.click"
|
const val BROADCAST_ACTION_WIDGET_CLICK = "com.v2ray.ang.action.widget.click"
|
||||||
|
|
||||||
/** Tasker extras. */
|
|
||||||
const val TASKER_EXTRA_BUNDLE = "com.twofortyfouram.locale.intent.extra.BUNDLE"
|
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_STRING_BLURB = "com.twofortyfouram.locale.intent.extra.BLURB"
|
||||||
const val TASKER_EXTRA_BUNDLE_SWITCH = "tasker_extra_bundle_switch"
|
const val TASKER_EXTRA_BUNDLE_SWITCH = "tasker_extra_bundle_switch"
|
||||||
const val TASKER_EXTRA_BUNDLE_GUID = "tasker_extra_bundle_guid"
|
const val TASKER_EXTRA_BUNDLE_GUID = "tasker_extra_bundle_guid"
|
||||||
const val TASKER_DEFAULT_GUID = "Default"
|
const val TASKER_DEFAULT_GUID = "Default"
|
||||||
|
|
||||||
/** Tags for different proxy modes. */
|
const val PREF_V2RAY_ROUTING_AGENT = "pref_v2ray_routing_agent"
|
||||||
const val TAG_PROXY = "proxy"
|
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_DIRECT = "direct"
|
||||||
const val TAG_BLOCKED = "block"
|
const val TAG_BLOCKED = "block"
|
||||||
const val TAG_FRAGMENT = "fragment"
|
|
||||||
|
|
||||||
/** Network-related constants. */
|
const val androidpackagenamelistUrl = "https://raw.githubusercontent.com/2dust/androidpackagenamelist/master/proxy.txt"
|
||||||
const val UPLINK = "uplink"
|
const val v2rayCustomRoutingListUrl = "https://raw.githubusercontent.com/2dust/v2rayCustomRoutingList/master/"
|
||||||
const val DOWNLINK = "downlink"
|
const val v2rayNGIssues = "https://github.com/2dust/v2rayNG/issues"
|
||||||
|
const val promotionUrl = "https://1.2345345.xyz/ads.html"
|
||||||
|
|
||||||
/** URLs for various resources. */
|
const val DNS_AGENT = "1.1.1.1"
|
||||||
const val androidpackagenamelistUrl =
|
|
||||||
"https://raw.githubusercontent.com/2dust/androidpackagenamelist/master/proxy.txt"
|
|
||||||
const val v2rayCustomRoutingListUrl =
|
|
||||||
"https://raw.githubusercontent.com/2dust/v2rayCustomRoutingList/master/"
|
|
||||||
const val v2rayNGUrl = "https://github.com/2dust/v2rayNG"
|
|
||||||
const val v2rayNGIssues = "$v2rayNGUrl/issues"
|
|
||||||
const val v2rayNGWikiMode = "$v2rayNGUrl/wiki/Mode"
|
|
||||||
const val v2rayNGPrivacyPolicy = "https://raw.githubusercontent.com/2dust/v2rayNG/master/CR.md"
|
|
||||||
const val PromotionUrl = "aHR0cHM6Ly85LjIzNDQ1Ni54eXovYWJjLmh0bWw="
|
|
||||||
const val GeoUrl = "https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/"
|
|
||||||
const val TgChannelUrl = "https://t.me/github_2dust"
|
|
||||||
const val DelayTestUrl = "https://www.gstatic.com/generate_204"
|
|
||||||
const val DelayTestUrl2 = "https://www.google.com/generate_204"
|
|
||||||
|
|
||||||
/** DNS server addresses. */
|
|
||||||
const val DNS_PROXY = "1.1.1.1"
|
|
||||||
const val DNS_DIRECT = "223.5.5.5"
|
const val DNS_DIRECT = "223.5.5.5"
|
||||||
const val DNS_VPN = "1.1.1.1"
|
|
||||||
const val GEOSITE_PRIVATE = "geosite:private"
|
|
||||||
const val GEOSITE_CN = "geosite:cn"
|
|
||||||
const val GEOIP_PRIVATE = "geoip:private"
|
|
||||||
const val GEOIP_CN = "geoip:cn"
|
|
||||||
|
|
||||||
/** Ports and addresses for various services. */
|
|
||||||
const val PORT_LOCAL_DNS = "10853"
|
|
||||||
const val PORT_SOCKS = "10808"
|
|
||||||
const val PORT_HTTP = "10809"
|
|
||||||
const val WIREGUARD_LOCAL_ADDRESS_V4 = "172.16.0.2/32"
|
|
||||||
const val WIREGUARD_LOCAL_ADDRESS_V6 = "2606:4700:110:8f81:d551:a0:532e:a2b3/128"
|
|
||||||
const val WIREGUARD_LOCAL_MTU = "1420"
|
|
||||||
const val LOOPBACK = "127.0.0.1"
|
|
||||||
|
|
||||||
/** Message constants for communication. */
|
|
||||||
const val MSG_REGISTER_CLIENT = 1
|
const val MSG_REGISTER_CLIENT = 1
|
||||||
const val MSG_STATE_RUNNING = 11
|
const val MSG_STATE_RUNNING = 11
|
||||||
const val MSG_STATE_NOT_RUNNING = 12
|
const val MSG_STATE_NOT_RUNNING = 12
|
||||||
@@ -128,55 +50,12 @@ object AppConfig {
|
|||||||
const val MSG_STATE_STOP = 4
|
const val MSG_STATE_STOP = 4
|
||||||
const val MSG_STATE_STOP_SUCCESS = 41
|
const val MSG_STATE_STOP_SUCCESS = 41
|
||||||
const val MSG_STATE_RESTART = 5
|
const val MSG_STATE_RESTART = 5
|
||||||
const val MSG_MEASURE_DELAY = 6
|
|
||||||
const val MSG_MEASURE_DELAY_SUCCESS = 61
|
|
||||||
const val MSG_MEASURE_CONFIG = 7
|
|
||||||
const val MSG_MEASURE_CONFIG_SUCCESS = 71
|
|
||||||
const val MSG_MEASURE_CONFIG_CANCEL = 72
|
|
||||||
|
|
||||||
/** Notification channel IDs and names. */
|
object EConfigType {
|
||||||
const val RAY_NG_CHANNEL_ID = "RAY_NG_M_CH_ID"
|
val Vmess = 1
|
||||||
const val RAY_NG_CHANNEL_NAME = "v2rayNG Background Service"
|
val Custom = 2
|
||||||
const val SUBSCRIPTION_UPDATE_CHANNEL = "subscription_update_channel"
|
val Shadowsocks = 3
|
||||||
const val SUBSCRIPTION_UPDATE_CHANNEL_NAME = "Subscription Update Service"
|
val Socks = 4
|
||||||
|
}
|
||||||
|
|
||||||
/** Protocols Scheme **/
|
}
|
||||||
const val VMESS = "vmess://"
|
|
||||||
const val CUSTOM = ""
|
|
||||||
const val SHADOWSOCKS = "ss://"
|
|
||||||
const val SOCKS = "socks://"
|
|
||||||
const val HTTP = "http://"
|
|
||||||
const val VLESS = "vless://"
|
|
||||||
const val TROJAN = "trojan://"
|
|
||||||
const val WIREGUARD = "wireguard://"
|
|
||||||
const val TUIC = "tuic://"
|
|
||||||
const val HYSTERIA2 = "hysteria2://"
|
|
||||||
const val HY2 = "hy2://"
|
|
||||||
|
|
||||||
/** Give a good name to this, IDK*/
|
|
||||||
const val VPN = "VPN"
|
|
||||||
|
|
||||||
// Google API rule constants
|
|
||||||
const val GOOGLEAPIS_CN_DOMAIN = "domain:googleapis.cn"
|
|
||||||
const val GOOGLEAPIS_COM_DOMAIN = "googleapis.com"
|
|
||||||
|
|
||||||
// Android Private DNS constants
|
|
||||||
const val DNS_PUB_DOMAIN = "dns.pub"
|
|
||||||
const val DNS_ALIDNS_DOMAIN = "dns.alidns.com"
|
|
||||||
const val DNS_ONE_ONE_DOMAIN = "one.one.one.one"
|
|
||||||
const val DNS_GOOGLE_DOMAIN = "dns.google"
|
|
||||||
|
|
||||||
val DNS_PUB_ADDRESSES = arrayListOf("1.12.12.12", "120.53.53.53")
|
|
||||||
val DNS_ALIDNS_ADDRESSES = arrayListOf("223.5.5.5", "223.6.6.6", "2400:3200::1", "2400:3200:baba::1")
|
|
||||||
val DNS_ONE_ONE_ADDRESSES = arrayListOf("1.1.1.1", "1.0.0.1", "2606:4700:4700::1111", "2606:4700:4700::1001")
|
|
||||||
val DNS_GOOGLE_ADDRESSES = arrayListOf("8.8.8.8", "8.8.4.4", "2001:4860:4860::8888", "2001:4860:4860::8844")
|
|
||||||
|
|
||||||
const val DEFAULT_PORT = 443
|
|
||||||
const val DEFAULT_SECURITY = "auto"
|
|
||||||
const val DEFAULT_LEVEL = 8
|
|
||||||
const val DEFAULT_NETWORK = "tcp"
|
|
||||||
const val TLS = "tls"
|
|
||||||
const val REALITY = "reality"
|
|
||||||
const val HEADER_TYPE_HTTP = "http"
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package com.v2ray.ang.dto
|
||||||
|
|
||||||
|
data class AngConfig(
|
||||||
|
var index: Int,
|
||||||
|
var vmess: ArrayList<VmessBean>,
|
||||||
|
var subItem: ArrayList<SubItemBean>
|
||||||
|
) {
|
||||||
|
data class VmessBean(var guid: String = "123456",
|
||||||
|
var address: String = "v2ray.cool",
|
||||||
|
var port: Int = 10086,
|
||||||
|
var id: String = "a3482e88-686a-4a58-8126-99c9df64b7bf",
|
||||||
|
var alterId: Int = 64,
|
||||||
|
var security: String = "aes-128-cfb",
|
||||||
|
var network: String = "tcp",
|
||||||
|
var remarks: String = "def",
|
||||||
|
var headerType: String = "",
|
||||||
|
var requestHost: String = "",
|
||||||
|
var path: String = "",
|
||||||
|
var streamSecurity: String = "",
|
||||||
|
var 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 = "")
|
||||||
|
}
|
||||||
@@ -2,10 +2,8 @@ package com.v2ray.ang.dto
|
|||||||
|
|
||||||
import android.graphics.drawable.Drawable
|
import android.graphics.drawable.Drawable
|
||||||
|
|
||||||
data class AppInfo(
|
data class AppInfo(val appName: String,
|
||||||
val appName: String,
|
val packageName: String,
|
||||||
val packageName: String,
|
val appIcon: Drawable,
|
||||||
val appIcon: Drawable,
|
val isSystemApp: Boolean,
|
||||||
val isSystemApp: Boolean,
|
var isSelected: Int)
|
||||||
var isSelected: Int
|
|
||||||
)
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
package com.v2ray.ang.dto
|
|
||||||
|
|
||||||
data class AssetUrlItem(
|
|
||||||
var remarks: String = "",
|
|
||||||
var url: String = "",
|
|
||||||
val addedTime: Long = System.currentTimeMillis(),
|
|
||||||
var lastUpdated: Long = -1
|
|
||||||
)
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
package com.v2ray.ang.dto
|
|
||||||
|
|
||||||
data class ConfigResult(
|
|
||||||
var status: Boolean,
|
|
||||||
var guid: String? = null,
|
|
||||||
var content: String = "",
|
|
||||||
var domainPort: String? = null,
|
|
||||||
)
|
|
||||||
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
package com.v2ray.ang.dto
|
|
||||||
|
|
||||||
import com.v2ray.ang.AppConfig
|
|
||||||
|
|
||||||
|
|
||||||
enum class EConfigType(val value: Int, val protocolScheme: String) {
|
|
||||||
VMESS(1, AppConfig.VMESS),
|
|
||||||
CUSTOM(2, AppConfig.CUSTOM),
|
|
||||||
SHADOWSOCKS(3, AppConfig.SHADOWSOCKS),
|
|
||||||
SOCKS(4, AppConfig.SOCKS),
|
|
||||||
VLESS(5, AppConfig.VLESS),
|
|
||||||
TROJAN(6, AppConfig.TROJAN),
|
|
||||||
WIREGUARD(7, AppConfig.WIREGUARD),
|
|
||||||
// TUIC(8, AppConfig.TUIC),
|
|
||||||
HYSTERIA2(9, AppConfig.HYSTERIA2),
|
|
||||||
HTTP(10, AppConfig.HTTP);
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
fun fromInt(value: Int) = entries.firstOrNull { it.value == value }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
package com.v2ray.ang.dto
|
|
||||||
|
|
||||||
data class Hysteria2Bean(
|
|
||||||
val server: String?,
|
|
||||||
val auth: String?,
|
|
||||||
val lazy: Boolean? = true,
|
|
||||||
val obfs: ObfsBean? = null,
|
|
||||||
val socks5: Socks5Bean? = null,
|
|
||||||
val http: Socks5Bean? = null,
|
|
||||||
val tls: TlsBean? = null,
|
|
||||||
val transport: TransportBean? = null,
|
|
||||||
) {
|
|
||||||
data class ObfsBean(
|
|
||||||
val type: String?,
|
|
||||||
val salamander: SalamanderBean?
|
|
||||||
) {
|
|
||||||
data class SalamanderBean(
|
|
||||||
val password: String?,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
data class Socks5Bean(
|
|
||||||
val listen: String?,
|
|
||||||
)
|
|
||||||
|
|
||||||
data class TlsBean(
|
|
||||||
val sni: String?,
|
|
||||||
val insecure: Boolean?,
|
|
||||||
val pinSHA256: String?,
|
|
||||||
)
|
|
||||||
|
|
||||||
data class TransportBean(
|
|
||||||
val type: String?,
|
|
||||||
val udp: TransportUdpBean?
|
|
||||||
) {
|
|
||||||
data class TransportUdpBean(
|
|
||||||
val hopInterval: String?,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
package com.v2ray.ang.dto
|
|
||||||
|
|
||||||
enum class Language(val code: String) {
|
|
||||||
AUTO("auto"),
|
|
||||||
ENGLISH("en"),
|
|
||||||
CHINA("zh-rCN"),
|
|
||||||
TRADITIONAL_CHINESE("zh-rTW"),
|
|
||||||
VIETNAMESE("vi"),
|
|
||||||
RUSSIAN("ru"),
|
|
||||||
PERSIAN("fa"),
|
|
||||||
BANGLA("bn");
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
fun fromCode(code: String): Language {
|
|
||||||
return entries.find { it.code == code } ?: AUTO
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
package com.v2ray.ang.dto
|
|
||||||
|
|
||||||
enum class NetworkType(val type: String) {
|
|
||||||
TCP("tcp"),
|
|
||||||
KCP("kcp"),
|
|
||||||
WS("ws"),
|
|
||||||
HTTP_UPGRADE("httpupgrade"),
|
|
||||||
SPLIT_HTTP("splithttp"),
|
|
||||||
HTTP("http"),
|
|
||||||
H2("h2"),
|
|
||||||
QUIC("quic"),
|
|
||||||
GRPC("grpc");
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
fun fromString(type: String?) = entries.find { it.type == type } ?: TCP
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
package com.v2ray.ang.dto
|
|
||||||
|
|
||||||
import com.v2ray.ang.AppConfig.TAG_BLOCKED
|
|
||||||
import com.v2ray.ang.AppConfig.TAG_DIRECT
|
|
||||||
import com.v2ray.ang.AppConfig.TAG_PROXY
|
|
||||||
import com.v2ray.ang.util.Utils
|
|
||||||
|
|
||||||
data class ProfileItem(
|
|
||||||
val configVersion: Int = 4,
|
|
||||||
val configType: EConfigType,
|
|
||||||
var subscriptionId: String = "",
|
|
||||||
var addedTime: Long = System.currentTimeMillis(),
|
|
||||||
|
|
||||||
var remarks: String = "",
|
|
||||||
var server: String? = null,
|
|
||||||
var serverPort: String? = null,
|
|
||||||
|
|
||||||
var password: String? = null,
|
|
||||||
var method: String? = null,
|
|
||||||
var flow: String? = null,
|
|
||||||
var username: String? = null,
|
|
||||||
|
|
||||||
var network: String? = null,
|
|
||||||
var headerType: String? = null,
|
|
||||||
var host: String? = null,
|
|
||||||
var path: String? = null,
|
|
||||||
var seed: String? = null,
|
|
||||||
var quicSecurity: String? = null,
|
|
||||||
var quicKey: String? = null,
|
|
||||||
var mode: String? = null,
|
|
||||||
var serviceName: String? = null,
|
|
||||||
var authority: String? = null,
|
|
||||||
|
|
||||||
var security: String? = null,
|
|
||||||
var sni: String? = null,
|
|
||||||
var alpn: String? = null,
|
|
||||||
var fingerPrint: String? = null,
|
|
||||||
var insecure: Boolean? = null,
|
|
||||||
|
|
||||||
var publicKey: String? = null,
|
|
||||||
var shortId: String? = null,
|
|
||||||
var spiderX: String? = null,
|
|
||||||
|
|
||||||
var secretKey: String? = null,
|
|
||||||
var localAddress: String? = null,
|
|
||||||
var reserved: String? = null,
|
|
||||||
var mtu: Int? = null,
|
|
||||||
|
|
||||||
var obfsPassword: String? = null,
|
|
||||||
var portHopping: String? = null,
|
|
||||||
var portHoppingInterval: String? = null,
|
|
||||||
var pinSHA256: String? = null,
|
|
||||||
|
|
||||||
) {
|
|
||||||
companion object {
|
|
||||||
fun create(configType: EConfigType): ProfileItem {
|
|
||||||
return ProfileItem(configType = configType)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getAllOutboundTags(): MutableList<String> {
|
|
||||||
return mutableListOf(TAG_PROXY, TAG_DIRECT, TAG_BLOCKED)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getServerAddressAndPort(): String {
|
|
||||||
return Utils.getIpv6Address(server) + ":" + serverPort
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getKeyProperty(): ProfileItem {
|
|
||||||
val copy = this.copy()
|
|
||||||
copy.subscriptionId = ""
|
|
||||||
copy.addedTime = 0L
|
|
||||||
return copy
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
package com.v2ray.ang.dto
|
|
||||||
|
|
||||||
data class ProfileLiteItem(
|
|
||||||
val configType: EConfigType,
|
|
||||||
var subscriptionId: String = "",
|
|
||||||
var remarks: String = "",
|
|
||||||
var server: String?,
|
|
||||||
var serverPort: Int?,
|
|
||||||
)
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
package com.v2ray.ang.dto
|
|
||||||
|
|
||||||
enum class RoutingType(val fileName: String) {
|
|
||||||
WHITE("custom_routing_white"),
|
|
||||||
BLACK("custom_routing_black"),
|
|
||||||
GLOBAL("custom_routing_global"),
|
|
||||||
WHITE_IRAN("custom_routing_white_iran");
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
fun fromIndex(index: Int): RoutingType {
|
|
||||||
return when (index) {
|
|
||||||
0 -> WHITE
|
|
||||||
1 -> BLACK
|
|
||||||
2 -> GLOBAL
|
|
||||||
3 -> WHITE_IRAN
|
|
||||||
else -> WHITE
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
package com.v2ray.ang.dto
|
|
||||||
|
|
||||||
data class RulesetItem(
|
|
||||||
var remarks: String? = "",
|
|
||||||
var ip: List<String>? = null,
|
|
||||||
var domain: List<String>? = null,
|
|
||||||
var outboundTag: String = "",
|
|
||||||
var port: String? = null,
|
|
||||||
var network: String? = null,
|
|
||||||
var protocol: List<String>? = null,
|
|
||||||
var enabled: Boolean = true,
|
|
||||||
var looked: Boolean? = false,
|
|
||||||
)
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
package com.v2ray.ang.dto
|
|
||||||
|
|
||||||
data class ServerAffiliationInfo(var testDelayMillis: Long = 0L) {
|
|
||||||
fun getTestDelayString(): String {
|
|
||||||
if (testDelayMillis == 0L) {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return testDelayMillis.toString() + "ms"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
package com.v2ray.ang.dto
|
|
||||||
|
|
||||||
import com.v2ray.ang.AppConfig.TAG_BLOCKED
|
|
||||||
import com.v2ray.ang.AppConfig.TAG_DIRECT
|
|
||||||
import com.v2ray.ang.AppConfig.TAG_PROXY
|
|
||||||
|
|
||||||
data class ServerConfig(
|
|
||||||
val configVersion: Int = 3,
|
|
||||||
val configType: EConfigType,
|
|
||||||
var subscriptionId: String = "",
|
|
||||||
val addedTime: Long = System.currentTimeMillis(),
|
|
||||||
var remarks: String = "",
|
|
||||||
val outboundBean: V2rayConfig.OutboundBean? = null,
|
|
||||||
var fullConfig: V2rayConfig? = null
|
|
||||||
) {
|
|
||||||
companion object {
|
|
||||||
fun create(configType: EConfigType): ServerConfig {
|
|
||||||
when (configType) {
|
|
||||||
EConfigType.VMESS,
|
|
||||||
EConfigType.VLESS ->
|
|
||||||
return ServerConfig(
|
|
||||||
configType = configType,
|
|
||||||
outboundBean = V2rayConfig.OutboundBean(
|
|
||||||
protocol = configType.name.lowercase(),
|
|
||||||
settings = V2rayConfig.OutboundBean.OutSettingsBean(
|
|
||||||
vnext = listOf(
|
|
||||||
V2rayConfig.OutboundBean.OutSettingsBean.VnextBean(
|
|
||||||
users = listOf(V2rayConfig.OutboundBean.OutSettingsBean.VnextBean.UsersBean())
|
|
||||||
)
|
|
||||||
)
|
|
||||||
),
|
|
||||||
streamSettings = V2rayConfig.OutboundBean.StreamSettingsBean()
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
EConfigType.CUSTOM ->
|
|
||||||
return ServerConfig(configType = configType)
|
|
||||||
|
|
||||||
EConfigType.SHADOWSOCKS,
|
|
||||||
EConfigType.SOCKS,
|
|
||||||
EConfigType.HTTP,
|
|
||||||
EConfigType.TROJAN,
|
|
||||||
EConfigType.HYSTERIA2 ->
|
|
||||||
return ServerConfig(
|
|
||||||
configType = configType,
|
|
||||||
outboundBean = V2rayConfig.OutboundBean(
|
|
||||||
protocol = configType.name.lowercase(),
|
|
||||||
settings = V2rayConfig.OutboundBean.OutSettingsBean(
|
|
||||||
servers = listOf(V2rayConfig.OutboundBean.OutSettingsBean.ServersBean())
|
|
||||||
),
|
|
||||||
streamSettings = V2rayConfig.OutboundBean.StreamSettingsBean()
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
EConfigType.WIREGUARD ->
|
|
||||||
return ServerConfig(
|
|
||||||
configType = configType,
|
|
||||||
outboundBean = V2rayConfig.OutboundBean(
|
|
||||||
protocol = configType.name.lowercase(),
|
|
||||||
settings = V2rayConfig.OutboundBean.OutSettingsBean(
|
|
||||||
secretKey = "",
|
|
||||||
peers = listOf(V2rayConfig.OutboundBean.OutSettingsBean.WireGuardBean())
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getProxyOutbound(): V2rayConfig.OutboundBean? {
|
|
||||||
if (configType != EConfigType.CUSTOM) {
|
|
||||||
return outboundBean
|
|
||||||
}
|
|
||||||
return fullConfig?.getProxyOutbound()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getAllOutboundTags(): MutableList<String> {
|
|
||||||
if (configType != EConfigType.CUSTOM) {
|
|
||||||
return mutableListOf(TAG_PROXY, TAG_DIRECT, TAG_BLOCKED)
|
|
||||||
}
|
|
||||||
fullConfig?.let { config ->
|
|
||||||
return config.outbounds.map { it.tag }.toMutableList()
|
|
||||||
}
|
|
||||||
return mutableListOf()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
package com.v2ray.ang.dto
|
|
||||||
|
|
||||||
data class ServersCache(
|
|
||||||
val guid: String,
|
|
||||||
val profile: ProfileItem
|
|
||||||
)
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
package com.v2ray.ang.dto
|
|
||||||
|
|
||||||
data class SubscriptionItem(
|
|
||||||
var remarks: String = "",
|
|
||||||
var url: String = "",
|
|
||||||
var enabled: Boolean = true,
|
|
||||||
val addedTime: Long = System.currentTimeMillis(),
|
|
||||||
var lastUpdated: Long = -1,
|
|
||||||
var autoUpdate: Boolean = false,
|
|
||||||
val updateInterval: Int? = null,
|
|
||||||
var prevProfile: String? = null,
|
|
||||||
var nextProfile: String? = null,
|
|
||||||
var filter: String? = null,
|
|
||||||
)
|
|
||||||
|
|
||||||
@@ -1,712 +1,142 @@
|
|||||||
package com.v2ray.ang.dto
|
package com.v2ray.ang.dto
|
||||||
|
|
||||||
import android.text.TextUtils
|
|
||||||
import com.google.gson.GsonBuilder
|
|
||||||
import com.google.gson.JsonPrimitive
|
|
||||||
import com.google.gson.JsonSerializationContext
|
|
||||||
import com.google.gson.JsonSerializer
|
|
||||||
import com.google.gson.annotations.SerializedName
|
|
||||||
import com.google.gson.reflect.TypeToken
|
|
||||||
import com.v2ray.ang.AppConfig
|
|
||||||
import com.v2ray.ang.dto.V2rayConfig.OutboundBean.OutSettingsBean.*
|
|
||||||
import com.v2ray.ang.dto.V2rayConfig.OutboundBean.OutSettingsBean.VnextBean.*
|
|
||||||
import com.v2ray.ang.util.Utils
|
|
||||||
import java.lang.reflect.Type
|
|
||||||
|
|
||||||
data class V2rayConfig(
|
data class V2rayConfig(
|
||||||
var remarks: String? = null,
|
val stats: Any?=null,
|
||||||
var stats: Any? = null,
|
val log: LogBean,
|
||||||
val log: LogBean,
|
val policy: PolicyBean,
|
||||||
var policy: PolicyBean?,
|
val inbounds: ArrayList<InboundBean>,
|
||||||
val inbounds: ArrayList<InboundBean>,
|
var outbounds: ArrayList<OutboundBean>,
|
||||||
var outbounds: ArrayList<OutboundBean>,
|
var dns: DnsBean,
|
||||||
var dns: DnsBean,
|
val routing: RoutingBean) {
|
||||||
val routing: RoutingBean,
|
|
||||||
val api: Any? = null,
|
|
||||||
val transport: Any? = null,
|
|
||||||
val reverse: Any? = null,
|
|
||||||
var fakedns: Any? = null,
|
|
||||||
val browserForwarder: Any? = null,
|
|
||||||
var observatory: Any? = null,
|
|
||||||
var burstObservatory: Any? = null
|
|
||||||
) {
|
|
||||||
|
|
||||||
data class LogBean(
|
data class LogBean(val access: String,
|
||||||
val access: String,
|
val error: String,
|
||||||
val error: String,
|
val loglevel: String)
|
||||||
var loglevel: String?,
|
|
||||||
val dnsLog: Boolean? = null
|
|
||||||
)
|
|
||||||
|
|
||||||
data class InboundBean(
|
data class InboundBean(
|
||||||
var tag: String,
|
var tag: String,
|
||||||
var port: Int,
|
var port: Int,
|
||||||
var protocol: String,
|
var protocol: String,
|
||||||
var listen: String? = null,
|
var listen: String?=null,
|
||||||
val settings: Any? = null,
|
val settings: InSettingsBean,
|
||||||
val sniffing: SniffingBean?,
|
val sniffing: SniffingBean?) {
|
||||||
val streamSettings: Any? = null,
|
|
||||||
val allocate: Any? = null
|
|
||||||
) {
|
|
||||||
|
|
||||||
data class InSettingsBean(
|
data class InSettingsBean(val auth: String? = null,
|
||||||
val auth: String? = null,
|
val udp: Boolean? = null,
|
||||||
val udp: Boolean? = null,
|
val userLevel: Int? =null,
|
||||||
val userLevel: Int? = null,
|
val address: String? = null,
|
||||||
val address: String? = null,
|
val port: Int? = null,
|
||||||
val port: Int? = null,
|
val network: String? = null)
|
||||||
val network: String? = null
|
|
||||||
)
|
|
||||||
|
|
||||||
data class SniffingBean(
|
data class SniffingBean(var enabled: Boolean,
|
||||||
var enabled: Boolean,
|
val destOverride: List<String>)
|
||||||
val destOverride: ArrayList<String>,
|
|
||||||
val metadataOnly: Boolean? = null,
|
|
||||||
var routeOnly: Boolean? = null
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
data class OutboundBean(
|
data class OutboundBean(val tag: String,
|
||||||
var tag: String = "proxy",
|
var protocol: String,
|
||||||
var protocol: String,
|
var settings: OutSettingsBean?,
|
||||||
var settings: OutSettingsBean? = null,
|
var streamSettings: StreamSettingsBean?,
|
||||||
var streamSettings: StreamSettingsBean? = null,
|
var mux: MuxBean?) {
|
||||||
val proxySettings: Any? = null,
|
|
||||||
val sendThrough: String? = null,
|
|
||||||
var mux: MuxBean? = MuxBean(false)
|
|
||||||
) {
|
|
||||||
companion object {
|
|
||||||
fun create(configType: EConfigType): OutboundBean? {
|
|
||||||
return when (configType) {
|
|
||||||
EConfigType.VMESS,
|
|
||||||
EConfigType.VLESS ->
|
|
||||||
return OutboundBean(
|
|
||||||
protocol = configType.name.lowercase(),
|
|
||||||
settings = OutSettingsBean(
|
|
||||||
vnext = listOf(
|
|
||||||
VnextBean(
|
|
||||||
users = listOf(UsersBean())
|
|
||||||
)
|
|
||||||
)
|
|
||||||
),
|
|
||||||
streamSettings = StreamSettingsBean()
|
|
||||||
)
|
|
||||||
|
|
||||||
EConfigType.SHADOWSOCKS,
|
data class OutSettingsBean(var vnext: List<VnextBean>?,
|
||||||
EConfigType.SOCKS,
|
var servers: List<ServersBean>?,
|
||||||
EConfigType.HTTP,
|
var response: Response) {
|
||||||
EConfigType.TROJAN,
|
|
||||||
EConfigType.HYSTERIA2 ->
|
|
||||||
return OutboundBean(
|
|
||||||
protocol = configType.name.lowercase(),
|
|
||||||
settings = OutSettingsBean(
|
|
||||||
servers = listOf(ServersBean())
|
|
||||||
),
|
|
||||||
streamSettings = StreamSettingsBean()
|
|
||||||
)
|
|
||||||
|
|
||||||
EConfigType.WIREGUARD ->
|
data class VnextBean(var address: String,
|
||||||
return OutboundBean(
|
var port: Int,
|
||||||
protocol = configType.name.lowercase(),
|
var users: List<UsersBean>) {
|
||||||
settings = OutSettingsBean(
|
|
||||||
secretKey = "",
|
|
||||||
peers = listOf(WireGuardBean())
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
EConfigType.CUSTOM -> null
|
data class UsersBean(var id: String,
|
||||||
}
|
var alterId: Int,
|
||||||
}
|
var security: String,
|
||||||
}
|
var level: Int)
|
||||||
|
|
||||||
data class OutSettingsBean(
|
|
||||||
var vnext: List<VnextBean>? = null,
|
|
||||||
var fragment: FragmentBean? = null,
|
|
||||||
var noises: List<NoiseBean>? = null,
|
|
||||||
var servers: List<ServersBean>? = null,
|
|
||||||
/*Blackhole*/
|
|
||||||
var response: Response? = null,
|
|
||||||
/*DNS*/
|
|
||||||
val network: String? = null,
|
|
||||||
var address: Any? = null,
|
|
||||||
val port: Int? = null,
|
|
||||||
/*Freedom*/
|
|
||||||
var domainStrategy: String? = null,
|
|
||||||
val redirect: String? = null,
|
|
||||||
val userLevel: Int? = null,
|
|
||||||
/*Loopback*/
|
|
||||||
val inboundTag: String? = null,
|
|
||||||
/*Wireguard*/
|
|
||||||
var secretKey: String? = null,
|
|
||||||
val peers: List<WireGuardBean>? = null,
|
|
||||||
var reserved: List<Int>? = null,
|
|
||||||
var mtu: Int? = null,
|
|
||||||
var obfsPassword: String? = null,
|
|
||||||
) {
|
|
||||||
|
|
||||||
data class VnextBean(
|
|
||||||
var address: String = "",
|
|
||||||
var port: Int = AppConfig.DEFAULT_PORT,
|
|
||||||
var users: List<UsersBean>
|
|
||||||
) {
|
|
||||||
|
|
||||||
data class UsersBean(
|
|
||||||
var id: String = "",
|
|
||||||
var alterId: Int? = null,
|
|
||||||
var security: String? = null,
|
|
||||||
var level: Int = AppConfig.DEFAULT_LEVEL,
|
|
||||||
var encryption: String? = null,
|
|
||||||
var flow: String? = null
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
data class FragmentBean(
|
data class ServersBean(var address: String,
|
||||||
var packets: String? = null,
|
var method: String,
|
||||||
var length: String? = null,
|
var ota: Boolean,
|
||||||
var interval: String? = null
|
var password: String,
|
||||||
)
|
var port: Int,
|
||||||
|
var level: Int)
|
||||||
data class NoiseBean(
|
|
||||||
var type: String? = null,
|
|
||||||
var packet: String? = null,
|
|
||||||
var delay: String? = null
|
|
||||||
)
|
|
||||||
|
|
||||||
data class ServersBean(
|
|
||||||
var address: String = "",
|
|
||||||
var method: String? = null,
|
|
||||||
var ota: Boolean = false,
|
|
||||||
var password: String? = null,
|
|
||||||
var port: Int = AppConfig.DEFAULT_PORT,
|
|
||||||
var level: Int = AppConfig.DEFAULT_LEVEL,
|
|
||||||
val email: String? = null,
|
|
||||||
var flow: String? = null,
|
|
||||||
val ivCheck: Boolean? = null,
|
|
||||||
var users: List<SocksUsersBean>? = null
|
|
||||||
) {
|
|
||||||
data class SocksUsersBean(
|
|
||||||
var user: String = "",
|
|
||||||
var pass: String = "",
|
|
||||||
var level: Int = AppConfig.DEFAULT_LEVEL
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
data class Response(var type: String)
|
data class Response(var type: String)
|
||||||
|
|
||||||
data class WireGuardBean(
|
|
||||||
var publicKey: String = "",
|
|
||||||
var endpoint: String = ""
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
data class StreamSettingsBean(
|
data class StreamSettingsBean(var network: String,
|
||||||
var network: String = AppConfig.DEFAULT_NETWORK,
|
var security: String,
|
||||||
var security: String = "",
|
var tcpSettings: TcpsettingsBean?,
|
||||||
var tcpSettings: TcpSettingsBean? = null,
|
var kcpsettings: KcpsettingsBean?,
|
||||||
var kcpSettings: KcpSettingsBean? = null,
|
var wssettings: WssettingsBean?,
|
||||||
var wsSettings: WsSettingsBean? = null,
|
var httpsettings: HttpsettingsBean?,
|
||||||
var httpupgradeSettings: HttpupgradeSettingsBean? = null,
|
var tlssettings: TlssettingsBean?,
|
||||||
var splithttpSettings: SplithttpSettingsBean? = null,
|
var quicsettings: QuicsettingBean?
|
||||||
var httpSettings: HttpSettingsBean? = null,
|
|
||||||
var tlsSettings: TlsSettingsBean? = null,
|
|
||||||
var quicSettings: QuicSettingBean? = null,
|
|
||||||
var realitySettings: TlsSettingsBean? = null,
|
|
||||||
var grpcSettings: GrpcSettingsBean? = null,
|
|
||||||
var hy2steriaSettings: Hy2steriaSettingsBean? = null,
|
|
||||||
val dsSettings: Any? = null,
|
|
||||||
var sockopt: SockoptBean? = null
|
|
||||||
) {
|
) {
|
||||||
|
|
||||||
data class TcpSettingsBean(
|
data class TcpsettingsBean(var connectionReuse: Boolean = true,
|
||||||
var header: HeaderBean = HeaderBean(),
|
var header: HeaderBean = HeaderBean()) {
|
||||||
val acceptProxyProtocol: Boolean? = null
|
data class HeaderBean(var type: String = "none",
|
||||||
) {
|
var request: Any? = null,
|
||||||
data class HeaderBean(
|
var response: Any? = null)
|
||||||
var type: String = "none",
|
|
||||||
var request: RequestBean? = null,
|
|
||||||
var response: Any? = null
|
|
||||||
) {
|
|
||||||
data class RequestBean(
|
|
||||||
var path: List<String> = ArrayList(),
|
|
||||||
var headers: HeadersBean = HeadersBean(),
|
|
||||||
val version: String? = null,
|
|
||||||
val method: String? = null
|
|
||||||
) {
|
|
||||||
data class HeadersBean(
|
|
||||||
var Host: List<String>? = ArrayList(),
|
|
||||||
@SerializedName("User-Agent")
|
|
||||||
val userAgent: List<String>? = null,
|
|
||||||
@SerializedName("Accept-Encoding")
|
|
||||||
val acceptEncoding: List<String>? = null,
|
|
||||||
val Connection: List<String>? = null,
|
|
||||||
val Pragma: String? = null
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
data class KcpSettingsBean(
|
data class KcpsettingsBean(var mtu: Int = 1350,
|
||||||
var mtu: Int = 1350,
|
var tti: Int = 20,
|
||||||
var tti: Int = 50,
|
var uplinkCapacity: Int = 12,
|
||||||
var uplinkCapacity: Int = 12,
|
var downlinkCapacity: Int = 100,
|
||||||
var downlinkCapacity: Int = 100,
|
var congestion: Boolean = false,
|
||||||
var congestion: Boolean = false,
|
var readBufferSize: Int = 1,
|
||||||
var readBufferSize: Int = 1,
|
var writeBufferSize: Int = 1,
|
||||||
var writeBufferSize: Int = 1,
|
var header: HeaderBean = HeaderBean()) {
|
||||||
var header: HeaderBean = HeaderBean(),
|
|
||||||
var seed: String? = null
|
|
||||||
) {
|
|
||||||
data class HeaderBean(var type: String = "none")
|
data class HeaderBean(var type: String = "none")
|
||||||
}
|
}
|
||||||
|
|
||||||
data class WsSettingsBean(
|
data class WssettingsBean(var connectionReuse: Boolean = true,
|
||||||
var path: String? = null,
|
var path: String = "",
|
||||||
var headers: HeadersBean = HeadersBean(),
|
var headers: HeadersBean = HeadersBean()) {
|
||||||
val maxEarlyData: Int? = null,
|
|
||||||
val useBrowserForwarding: Boolean? = null,
|
|
||||||
val acceptProxyProtocol: Boolean? = null
|
|
||||||
) {
|
|
||||||
data class HeadersBean(var Host: String = "")
|
data class HeadersBean(var Host: String = "")
|
||||||
}
|
}
|
||||||
|
|
||||||
data class HttpupgradeSettingsBean(
|
data class HttpsettingsBean(var host: List<String> = ArrayList<String>(), var path: String = "")
|
||||||
var path: String? = null,
|
|
||||||
var host: String? = null,
|
|
||||||
val acceptProxyProtocol: Boolean? = null
|
|
||||||
)
|
|
||||||
|
|
||||||
data class SplithttpSettingsBean(
|
data class TlssettingsBean(var allowInsecure: Boolean = true,
|
||||||
var path: String? = null,
|
var serverName: String = "")
|
||||||
var host: String? = null,
|
|
||||||
val maxUploadSize: Int? = null,
|
|
||||||
val maxConcurrentUploads: Int? = null
|
|
||||||
)
|
|
||||||
|
|
||||||
data class HttpSettingsBean(
|
data class QuicsettingBean(var security: String = "none",
|
||||||
var host: List<String> = ArrayList(),
|
var key: String = "",
|
||||||
var path: String? = null
|
var header: HeaderBean = HeaderBean()) {
|
||||||
)
|
|
||||||
|
|
||||||
data class SockoptBean(
|
|
||||||
var TcpNoDelay: Boolean? = null,
|
|
||||||
var tcpKeepAliveIdle: Int? = null,
|
|
||||||
var tcpFastOpen: Boolean? = null,
|
|
||||||
var tproxy: String? = null,
|
|
||||||
var mark: Int? = null,
|
|
||||||
var dialerProxy: String? = null
|
|
||||||
)
|
|
||||||
|
|
||||||
data class TlsSettingsBean(
|
|
||||||
var allowInsecure: Boolean = false,
|
|
||||||
var serverName: String? = null,
|
|
||||||
val alpn: List<String>? = null,
|
|
||||||
val minVersion: String? = null,
|
|
||||||
val maxVersion: String? = null,
|
|
||||||
val preferServerCipherSuites: Boolean? = null,
|
|
||||||
val cipherSuites: String? = null,
|
|
||||||
val fingerprint: String? = null,
|
|
||||||
val certificates: List<Any>? = null,
|
|
||||||
val disableSystemRoot: Boolean? = null,
|
|
||||||
val enableSessionResumption: Boolean? = null,
|
|
||||||
// REALITY settings
|
|
||||||
val show: Boolean = false,
|
|
||||||
var publicKey: String? = null,
|
|
||||||
var shortId: String? = null,
|
|
||||||
var spiderX: String? = null
|
|
||||||
)
|
|
||||||
|
|
||||||
data class QuicSettingBean(
|
|
||||||
var security: String = "none",
|
|
||||||
var key: String = "",
|
|
||||||
var header: HeaderBean = HeaderBean()
|
|
||||||
) {
|
|
||||||
data class HeaderBean(var type: String = "none")
|
data class HeaderBean(var type: String = "none")
|
||||||
}
|
}
|
||||||
|
|
||||||
data class GrpcSettingsBean(
|
|
||||||
var serviceName: String = "",
|
|
||||||
var authority: String? = null,
|
|
||||||
var multiMode: Boolean? = null,
|
|
||||||
var idle_timeout: Int? = null,
|
|
||||||
var health_check_timeout: Int? = null
|
|
||||||
)
|
|
||||||
|
|
||||||
data class Hy2steriaSettingsBean(
|
|
||||||
var password: String? = null,
|
|
||||||
var use_udp_extension: Boolean? = true,
|
|
||||||
var congestion: Hy2CongestionBean? = null
|
|
||||||
) {
|
|
||||||
data class Hy2CongestionBean(
|
|
||||||
var type: String? = "bbr",
|
|
||||||
var up_mbps: Int? = null,
|
|
||||||
var down_mbps: Int? = null,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun populateTransportSettings(
|
|
||||||
transport: String, headerType: String?, host: String?, path: String?, seed: String?,
|
|
||||||
quicSecurity: String?, key: String?, mode: String?, serviceName: String?,
|
|
||||||
authority: String?
|
|
||||||
): String? {
|
|
||||||
var sni: String? = null
|
|
||||||
network = transport
|
|
||||||
when (network) {
|
|
||||||
"tcp" -> {
|
|
||||||
val tcpSetting = TcpSettingsBean()
|
|
||||||
if (headerType == AppConfig.HEADER_TYPE_HTTP) {
|
|
||||||
tcpSetting.header.type = AppConfig.HEADER_TYPE_HTTP
|
|
||||||
if (!TextUtils.isEmpty(host) || !TextUtils.isEmpty(path)) {
|
|
||||||
val requestObj = TcpSettingsBean.HeaderBean.RequestBean()
|
|
||||||
requestObj.headers.Host = host.orEmpty().split(",").map { it.trim() }.filter { it.isNotEmpty() }
|
|
||||||
requestObj.path = path.orEmpty().split(",").map { it.trim() }.filter { it.isNotEmpty() }
|
|
||||||
tcpSetting.header.request = requestObj
|
|
||||||
sni = requestObj.headers.Host?.getOrNull(0) ?: sni
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
tcpSetting.header.type = "none"
|
|
||||||
sni = host.orEmpty()
|
|
||||||
}
|
|
||||||
tcpSettings = tcpSetting
|
|
||||||
}
|
|
||||||
|
|
||||||
"kcp" -> {
|
|
||||||
val kcpsetting = KcpSettingsBean()
|
|
||||||
kcpsetting.header.type = headerType ?: "none"
|
|
||||||
if (seed.isNullOrEmpty()) {
|
|
||||||
kcpsetting.seed = null
|
|
||||||
} else {
|
|
||||||
kcpsetting.seed = seed
|
|
||||||
}
|
|
||||||
kcpSettings = kcpsetting
|
|
||||||
}
|
|
||||||
|
|
||||||
"ws" -> {
|
|
||||||
val wssetting = WsSettingsBean()
|
|
||||||
wssetting.headers.Host = host.orEmpty()
|
|
||||||
sni = wssetting.headers.Host
|
|
||||||
wssetting.path = path ?: "/"
|
|
||||||
wsSettings = wssetting
|
|
||||||
}
|
|
||||||
|
|
||||||
"httpupgrade" -> {
|
|
||||||
val httpupgradeSetting = HttpupgradeSettingsBean()
|
|
||||||
httpupgradeSetting.host = host.orEmpty()
|
|
||||||
sni = httpupgradeSetting.host
|
|
||||||
httpupgradeSetting.path = path ?: "/"
|
|
||||||
httpupgradeSettings = httpupgradeSetting
|
|
||||||
}
|
|
||||||
|
|
||||||
"splithttp" -> {
|
|
||||||
val splithttpSetting = SplithttpSettingsBean()
|
|
||||||
splithttpSetting.host = host.orEmpty()
|
|
||||||
sni = splithttpSetting.host
|
|
||||||
splithttpSetting.path = path ?: "/"
|
|
||||||
splithttpSettings = splithttpSetting
|
|
||||||
}
|
|
||||||
|
|
||||||
"h2", "http" -> {
|
|
||||||
network = "h2"
|
|
||||||
val h2Setting = HttpSettingsBean()
|
|
||||||
h2Setting.host = host.orEmpty().split(",").map { it.trim() }.filter { it.isNotEmpty() }
|
|
||||||
sni = h2Setting.host.getOrNull(0) ?: sni
|
|
||||||
h2Setting.path = path ?: "/"
|
|
||||||
httpSettings = h2Setting
|
|
||||||
}
|
|
||||||
|
|
||||||
"quic" -> {
|
|
||||||
val quicsetting = QuicSettingBean()
|
|
||||||
quicsetting.security = quicSecurity ?: "none"
|
|
||||||
quicsetting.key = key.orEmpty()
|
|
||||||
quicsetting.header.type = headerType ?: "none"
|
|
||||||
quicSettings = quicsetting
|
|
||||||
}
|
|
||||||
|
|
||||||
"grpc" -> {
|
|
||||||
val grpcSetting = GrpcSettingsBean()
|
|
||||||
grpcSetting.multiMode = mode == "multi"
|
|
||||||
grpcSetting.serviceName = serviceName.orEmpty()
|
|
||||||
grpcSetting.authority = authority.orEmpty()
|
|
||||||
grpcSetting.idle_timeout = 60
|
|
||||||
grpcSetting.health_check_timeout = 20
|
|
||||||
sni = authority.orEmpty()
|
|
||||||
grpcSettings = grpcSetting
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return sni
|
|
||||||
}
|
|
||||||
|
|
||||||
fun populateTlsSettings(
|
|
||||||
streamSecurity: String, allowInsecure: Boolean, sni: String?, fingerprint: String?, alpns: String?,
|
|
||||||
publicKey: String?, shortId: String?, spiderX: String?
|
|
||||||
) {
|
|
||||||
security = streamSecurity
|
|
||||||
val tlsSetting = TlsSettingsBean(
|
|
||||||
allowInsecure = allowInsecure,
|
|
||||||
serverName = sni,
|
|
||||||
fingerprint = fingerprint,
|
|
||||||
alpn = if (alpns.isNullOrEmpty()) null else alpns.split(",").map { it.trim() }.filter { it.isNotEmpty() },
|
|
||||||
publicKey = publicKey,
|
|
||||||
shortId = shortId,
|
|
||||||
spiderX = spiderX
|
|
||||||
)
|
|
||||||
if (security == AppConfig.TLS) {
|
|
||||||
tlsSettings = tlsSetting
|
|
||||||
realitySettings = null
|
|
||||||
} else if (security == AppConfig.REALITY) {
|
|
||||||
tlsSettings = null
|
|
||||||
realitySettings = tlsSetting
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
data class MuxBean(
|
data class MuxBean(var enabled: Boolean)
|
||||||
var enabled: Boolean,
|
|
||||||
var concurrency: Int = 8,
|
|
||||||
var xudpConcurrency: Int = 8,
|
|
||||||
var xudpProxyUDP443: String = "",
|
|
||||||
)
|
|
||||||
|
|
||||||
fun getServerAddress(): String? {
|
|
||||||
if (protocol.equals(EConfigType.VMESS.name, true)
|
|
||||||
|| protocol.equals(EConfigType.VLESS.name, true)
|
|
||||||
) {
|
|
||||||
return settings?.vnext?.get(0)?.address
|
|
||||||
} else if (protocol.equals(EConfigType.SHADOWSOCKS.name, true)
|
|
||||||
|| protocol.equals(EConfigType.SOCKS.name, true)
|
|
||||||
|| protocol.equals(EConfigType.HTTP.name, true)
|
|
||||||
|| protocol.equals(EConfigType.TROJAN.name, true)
|
|
||||||
|| protocol.equals(EConfigType.HYSTERIA2.name, true)
|
|
||||||
) {
|
|
||||||
return settings?.servers?.get(0)?.address
|
|
||||||
} else if (protocol.equals(EConfigType.WIREGUARD.name, true)) {
|
|
||||||
return settings?.peers?.get(0)?.endpoint?.substringBeforeLast(":")
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getServerPort(): Int? {
|
|
||||||
if (protocol.equals(EConfigType.VMESS.name, true)
|
|
||||||
|| protocol.equals(EConfigType.VLESS.name, true)
|
|
||||||
) {
|
|
||||||
return settings?.vnext?.get(0)?.port
|
|
||||||
} else if (protocol.equals(EConfigType.SHADOWSOCKS.name, true)
|
|
||||||
|| protocol.equals(EConfigType.SOCKS.name, true)
|
|
||||||
|| protocol.equals(EConfigType.HTTP.name, true)
|
|
||||||
|| protocol.equals(EConfigType.TROJAN.name, true)
|
|
||||||
|| protocol.equals(EConfigType.HYSTERIA2.name, true)
|
|
||||||
) {
|
|
||||||
return settings?.servers?.get(0)?.port
|
|
||||||
} else if (protocol.equals(EConfigType.WIREGUARD.name, true)) {
|
|
||||||
return settings?.peers?.get(0)?.endpoint?.substringAfterLast(":")?.toInt()
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getServerAddressAndPort(): String {
|
|
||||||
val address = getServerAddress().orEmpty()
|
|
||||||
val port = getServerPort()
|
|
||||||
return Utils.getIpv6Address(address) + ":" + port
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getPassword(): String? {
|
|
||||||
if (protocol.equals(EConfigType.VMESS.name, true)
|
|
||||||
|| protocol.equals(EConfigType.VLESS.name, true)
|
|
||||||
) {
|
|
||||||
return settings?.vnext?.get(0)?.users?.get(0)?.id
|
|
||||||
} else if (protocol.equals(EConfigType.SHADOWSOCKS.name, true)
|
|
||||||
|| protocol.equals(EConfigType.TROJAN.name, true)
|
|
||||||
|| protocol.equals(EConfigType.HYSTERIA2.name, true)
|
|
||||||
) {
|
|
||||||
return settings?.servers?.get(0)?.password
|
|
||||||
} else if (protocol.equals(EConfigType.SOCKS.name, true)
|
|
||||||
|| protocol.equals(EConfigType.HTTP.name, true)
|
|
||||||
) {
|
|
||||||
return settings?.servers?.get(0)?.users?.get(0)?.pass
|
|
||||||
} else if (protocol.equals(EConfigType.WIREGUARD.name, true)) {
|
|
||||||
return settings?.secretKey
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getSecurityEncryption(): String? {
|
|
||||||
return when {
|
|
||||||
protocol.equals(EConfigType.VMESS.name, true) -> settings?.vnext?.get(0)?.users?.get(0)?.security
|
|
||||||
protocol.equals(EConfigType.VLESS.name, true) -> settings?.vnext?.get(0)?.users?.get(0)?.encryption
|
|
||||||
protocol.equals(EConfigType.SHADOWSOCKS.name, true) -> settings?.servers?.get(0)?.method
|
|
||||||
else -> null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getTransportSettingDetails(): List<String?>? {
|
|
||||||
if (protocol.equals(EConfigType.VMESS.name, true)
|
|
||||||
|| protocol.equals(EConfigType.VLESS.name, true)
|
|
||||||
|| protocol.equals(EConfigType.TROJAN.name, true)
|
|
||||||
|| protocol.equals(EConfigType.SHADOWSOCKS.name, true)
|
|
||||||
) {
|
|
||||||
val transport = streamSettings?.network ?: return null
|
|
||||||
return when (transport) {
|
|
||||||
"tcp" -> {
|
|
||||||
val tcpSetting = streamSettings?.tcpSettings ?: return null
|
|
||||||
listOf(
|
|
||||||
tcpSetting.header.type,
|
|
||||||
tcpSetting.header.request?.headers?.Host?.joinToString(",").orEmpty(),
|
|
||||||
tcpSetting.header.request?.path?.joinToString(",").orEmpty()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
"kcp" -> {
|
|
||||||
val kcpSetting = streamSettings?.kcpSettings ?: return null
|
|
||||||
listOf(
|
|
||||||
kcpSetting.header.type,
|
|
||||||
"",
|
|
||||||
kcpSetting.seed.orEmpty()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
"ws" -> {
|
|
||||||
val wsSetting = streamSettings?.wsSettings ?: return null
|
|
||||||
listOf(
|
|
||||||
"",
|
|
||||||
wsSetting.headers.Host,
|
|
||||||
wsSetting.path
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
"httpupgrade" -> {
|
|
||||||
val httpupgradeSetting = streamSettings?.httpupgradeSettings ?: return null
|
|
||||||
listOf(
|
|
||||||
"",
|
|
||||||
httpupgradeSetting.host,
|
|
||||||
httpupgradeSetting.path
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
"splithttp" -> {
|
|
||||||
val splithttpSetting = streamSettings?.splithttpSettings ?: return null
|
|
||||||
listOf(
|
|
||||||
"",
|
|
||||||
splithttpSetting.host,
|
|
||||||
splithttpSetting.path
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
"h2" -> {
|
|
||||||
val h2Setting = streamSettings?.httpSettings ?: return null
|
|
||||||
listOf(
|
|
||||||
"",
|
|
||||||
h2Setting.host.joinToString(","),
|
|
||||||
h2Setting.path
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
"quic" -> {
|
|
||||||
val quicSetting = streamSettings?.quicSettings ?: return null
|
|
||||||
listOf(
|
|
||||||
quicSetting.header.type,
|
|
||||||
quicSetting.security,
|
|
||||||
quicSetting.key
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
"grpc" -> {
|
|
||||||
val grpcSetting = streamSettings?.grpcSettings ?: return null
|
|
||||||
listOf(
|
|
||||||
if (grpcSetting.multiMode == true) "multi" else "gun",
|
|
||||||
grpcSetting.authority.orEmpty(),
|
|
||||||
grpcSetting.serviceName
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
data class DnsBean(
|
//data class DnsBean(var servers: List<String>)
|
||||||
var servers: ArrayList<Any>? = null,
|
data class DnsBean(var servers: List<Any>?=null,
|
||||||
var hosts: Map<String, Any>? = null,
|
var hosts: Map<String, String>?=null
|
||||||
val clientIp: String? = null,
|
|
||||||
val disableCache: Boolean? = null,
|
|
||||||
val queryStrategy: String? = null,
|
|
||||||
val tag: String? = null
|
|
||||||
) {
|
) {
|
||||||
data class ServersBean(
|
data class ServersBean(var address: String = "",
|
||||||
var address: String = "",
|
var port: Int = 0,
|
||||||
var port: Int? = null,
|
var domains: List<String>?)
|
||||||
var domains: List<String>? = null,
|
|
||||||
var expectIPs: List<String>? = null,
|
|
||||||
val clientIp: String? = null,
|
|
||||||
val skipFallback: Boolean? = null,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
data class RoutingBean(
|
data class RoutingBean(var domainStrategy: String,
|
||||||
var domainStrategy: String,
|
var rules: ArrayList<RulesBean>) {
|
||||||
var domainMatcher: String? = null,
|
|
||||||
var rules: ArrayList<RulesBean>,
|
|
||||||
val balancers: List<Any>? = null
|
|
||||||
) {
|
|
||||||
|
|
||||||
data class RulesBean(
|
data class RulesBean(var type: String = "",
|
||||||
var type: String = "field",
|
var ip: ArrayList<String>? = null,
|
||||||
var ip: ArrayList<String>? = null,
|
var domain: ArrayList<String>? = null,
|
||||||
var domain: ArrayList<String>? = null,
|
var outboundTag: String = "",
|
||||||
var outboundTag: String = "",
|
var port: String? = null,
|
||||||
var balancerTag: String? = null,
|
var inboundTag: ArrayList<String>? = null)
|
||||||
var port: String? = null,
|
|
||||||
val sourcePort: String? = null,
|
|
||||||
val network: String? = null,
|
|
||||||
val source: List<String>? = null,
|
|
||||||
val user: List<String>? = null,
|
|
||||||
var inboundTag: List<String>? = null,
|
|
||||||
val protocol: List<String>? = null,
|
|
||||||
val attrs: String? = null,
|
|
||||||
val domainMatcher: String? = null
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
data class PolicyBean(
|
data class PolicyBean(var levels: Map<String, LevelBean>,
|
||||||
var levels: Map<String, LevelBean>,
|
var system: Any?=null) {
|
||||||
var system: Any? = null
|
|
||||||
) {
|
|
||||||
data class LevelBean(
|
data class LevelBean(
|
||||||
var handshake: Int? = null,
|
var handshake: Int? = null,
|
||||||
var connIdle: Int? = null,
|
var connIdle: Int? = null,
|
||||||
var uplinkOnly: Int? = null,
|
var uplinkOnly: Int? = null,
|
||||||
var downlinkOnly: Int? = null,
|
var downlinkOnly: Int? = null)
|
||||||
val statsUserUplink: Boolean? = null,
|
|
||||||
val statsUserDownlink: Boolean? = null,
|
|
||||||
var bufferSize: Int? = null
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
data class FakednsBean(
|
|
||||||
var ipPool: String = "198.18.0.0/15",
|
|
||||||
var poolSize: Int = 10000
|
|
||||||
) // roughly 10 times smaller than total ip pool
|
|
||||||
|
|
||||||
fun getProxyOutbound(): OutboundBean? {
|
|
||||||
outbounds.forEach { outbound ->
|
|
||||||
EConfigType.entries.forEach {
|
|
||||||
if (outbound.protocol.equals(it.name, true)) {
|
|
||||||
return outbound
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
fun toPrettyPrinting(): String {
|
|
||||||
return GsonBuilder()
|
|
||||||
.setPrettyPrinting()
|
|
||||||
.disableHtmlEscaping()
|
|
||||||
.registerTypeAdapter( // custom serialiser is needed here since JSON by default parse number as Double, core will fail to start
|
|
||||||
object : TypeToken<Double>() {}.type,
|
|
||||||
JsonSerializer { src: Double?, _: Type?, _: JsonSerializationContext? -> JsonPrimitive(src?.toInt()) }
|
|
||||||
)
|
|
||||||
.create()
|
|
||||||
.toJson(this)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,19 +1,13 @@
|
|||||||
package com.v2ray.ang.dto
|
package com.v2ray.ang.dto
|
||||||
|
|
||||||
data class VmessQRCode(
|
data class VmessQRCode(var v: String = "",
|
||||||
var v: String = "",
|
var ps: String = "",
|
||||||
var ps: String = "",
|
var add: String = "",
|
||||||
var add: String = "",
|
var port: String = "",
|
||||||
var port: String = "",
|
var id: String = "",
|
||||||
var id: String = "",
|
var aid: String = "",
|
||||||
var aid: String = "0",
|
var net: String = "",
|
||||||
var scy: String = "",
|
var type: String = "",
|
||||||
var net: String = "",
|
var host: String = "",
|
||||||
var type: String = "",
|
var path: String = "",
|
||||||
var host: String = "",
|
var tls: String = "")
|
||||||
var path: String = "",
|
|
||||||
var tls: String = "",
|
|
||||||
var sni: String = "",
|
|
||||||
var alpn: String = "",
|
|
||||||
var fp: String = ""
|
|
||||||
)
|
|
||||||
@@ -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<CharSequence>,
|
||||||
|
onClick: (Int) -> Unit
|
||||||
|
): Unit = activity.selector(title, items, onClick)
|
||||||
|
|
||||||
|
fun Context.selector(
|
||||||
|
title: CharSequence? = null,
|
||||||
|
items: List<CharSequence>,
|
||||||
|
onClick: (Int) -> Unit
|
||||||
|
) {
|
||||||
|
with(KAlertDialogBuilder(this)) {
|
||||||
|
if (title != null) title(title)
|
||||||
|
items(items, onClick)
|
||||||
|
show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class KAlertDialogBuilder(val ctx: Context) {
|
||||||
|
|
||||||
|
val builder: AlertDialog.Builder = AlertDialog.Builder(ctx)
|
||||||
|
protected var dialog: AlertDialog? = null
|
||||||
|
|
||||||
|
fun dismiss() {
|
||||||
|
dialog?.dismiss()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun show(): KAlertDialogBuilder {
|
||||||
|
dialog = builder.create()
|
||||||
|
dialog!!.show()
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun title(title: CharSequence) {
|
||||||
|
builder.setTitle(title)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun title(resource: Int) {
|
||||||
|
builder.setTitle(resource)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun message(title: CharSequence) {
|
||||||
|
builder.setMessage(title)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun message(resource: Int) {
|
||||||
|
builder.setMessage(resource)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun icon(icon: Int) {
|
||||||
|
builder.setIcon(icon)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun icon(icon: Drawable) {
|
||||||
|
builder.setIcon(icon)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun customTitle(title: View) {
|
||||||
|
builder.setCustomTitle(title)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun customView(view: View) {
|
||||||
|
builder.setView(view)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun cancellable(value: Boolean = true) {
|
||||||
|
builder.setCancelable(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onCancel(f: () -> Unit) {
|
||||||
|
builder.setOnCancelListener { f() }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onKey(f: (keyCode: Int, e: KeyEvent) -> Boolean) {
|
||||||
|
builder.setOnKeyListener({ dialog, keyCode, event -> f(keyCode, event) })
|
||||||
|
}
|
||||||
|
|
||||||
|
fun neutralButton(textResource: Int = android.R.string.ok, f: DialogInterface.() -> Unit = { dismiss() }) {
|
||||||
|
neutralButton(ctx.getString(textResource), f)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun neutralButton(title: String, f: DialogInterface.() -> Unit = { dismiss() }) {
|
||||||
|
builder.setNeutralButton(title, { dialog, which -> dialog.f() })
|
||||||
|
}
|
||||||
|
|
||||||
|
fun positiveButton(textResource: Int = android.R.string.ok, f: DialogInterface.() -> Unit) {
|
||||||
|
positiveButton(ctx.getString(textResource), f)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun positiveButton(title: String, f: DialogInterface.() -> Unit) {
|
||||||
|
builder.setPositiveButton(title, { dialog, which -> dialog.f() })
|
||||||
|
}
|
||||||
|
|
||||||
|
fun negativeButton(textResource: Int = android.R.string.cancel, f: DialogInterface.() -> Unit = { dismiss() }) {
|
||||||
|
negativeButton(ctx.getString(textResource), f)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun negativeButton(title: String, f: DialogInterface.() -> Unit = { dismiss() }) {
|
||||||
|
builder.setNegativeButton(title, { dialog, which -> dialog.f() })
|
||||||
|
}
|
||||||
|
|
||||||
|
fun items(itemsId: Int, f: (which: Int) -> Unit) {
|
||||||
|
items(ctx.resources!!.getTextArray(itemsId), f)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun items(items: List<CharSequence>, f: (which: Int) -> Unit) {
|
||||||
|
items(items.toTypedArray(), f)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun items(items: Array<CharSequence>, f: (which: Int) -> Unit) {
|
||||||
|
builder.setItems(items, { dialog, which -> f(which) })
|
||||||
|
}
|
||||||
|
|
||||||
|
fun adapter(adapter: ListAdapter, f: (which: Int) -> Unit) {
|
||||||
|
builder.setAdapter(adapter, { dialog, which -> f(which) })
|
||||||
|
}
|
||||||
|
|
||||||
|
fun adapter(cursor: Cursor, labelColumn: String, f: (which: Int) -> Unit) {
|
||||||
|
builder.setCursor(cursor, { dialog, which -> f(which) }, labelColumn)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,98 +1,64 @@
|
|||||||
package com.v2ray.ang.extension
|
package com.v2ray.ang.extension
|
||||||
|
|
||||||
import android.content.BroadcastReceiver
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
|
||||||
import android.content.IntentFilter
|
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
|
||||||
import android.widget.Toast
|
|
||||||
import com.v2ray.ang.AngApplication
|
import com.v2ray.ang.AngApplication
|
||||||
import me.drakeet.support.toast.ToastCompat
|
import me.dozen.dpreference.DPreference
|
||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
import java.io.Serializable
|
|
||||||
import java.net.URI
|
|
||||||
import java.net.URLConnection
|
import java.net.URLConnection
|
||||||
|
|
||||||
val Context.v2RayApplication: AngApplication?
|
/**
|
||||||
get() = applicationContext as? AngApplication
|
* Some extensions
|
||||||
|
*/
|
||||||
|
|
||||||
fun Context.toast(message: Int) {
|
val Context.v2RayApplication: AngApplication
|
||||||
ToastCompat.makeText(this, message, Toast.LENGTH_SHORT).show()
|
get() = applicationContext as AngApplication
|
||||||
}
|
|
||||||
|
|
||||||
fun Context.toast(message: CharSequence) {
|
val Context.defaultDPreference: DPreference
|
||||||
ToastCompat.makeText(this, message, Toast.LENGTH_SHORT).show()
|
get() = v2RayApplication.defaultDPreference
|
||||||
}
|
|
||||||
|
|
||||||
fun JSONObject.putOpt(pair: Pair<String, Any?>) {
|
|
||||||
put(pair.first, pair.second)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun JSONObject.putOpt(pairs: Map<String, Any?>) {
|
fun JSONObject.putOpt(pair: Pair<String, Any>) = putOpt(pair.first, pair.second)!!
|
||||||
pairs.forEach { put(it.key, it.value) }
|
fun JSONObject.putOpt(pairs: Map<String, Any>) = pairs.forEach { putOpt(it.key to it.value) }
|
||||||
}
|
|
||||||
|
|
||||||
const val THRESHOLD = 1000L
|
const val threshold = 1000
|
||||||
const val DIVISOR = 1024.0
|
const val divisor = 1024F
|
||||||
|
|
||||||
fun Long.toSpeedString(): String = this.toTrafficString() + "/s"
|
fun Long.toSpeedString() = toTrafficString() + "/s"
|
||||||
|
|
||||||
fun Long.toTrafficString(): String {
|
fun Long.toTrafficString(): String {
|
||||||
val units = arrayOf("B", "KB", "MB", "GB", "TB", "PB")
|
if (this < threshold)
|
||||||
var size = this.toDouble()
|
return "$this B"
|
||||||
var unitIndex = 0
|
|
||||||
while (size >= THRESHOLD && unitIndex < units.size - 1) {
|
val kib = this / divisor
|
||||||
size /= DIVISOR
|
if (kib < threshold)
|
||||||
unitIndex++
|
return "${kib.toShortString()} KB"
|
||||||
}
|
|
||||||
return String.format("%.1f %s", size, units[unitIndex])
|
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
|
val URLConnection.responseLength: Long
|
||||||
get() = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
get() = if (Build.VERSION.SDK_INT >= 24) contentLengthLong else contentLength.toLong()
|
||||||
contentLengthLong
|
|
||||||
} else {
|
|
||||||
contentLength.toLong()
|
|
||||||
}
|
|
||||||
|
|
||||||
val URI.idnHost: String
|
|
||||||
get() = host?.replace("[", "")?.replace("]", "").orEmpty()
|
|
||||||
|
|
||||||
fun String.removeWhiteSpace(): String = replace("\\s+".toRegex(), "")
|
|
||||||
|
|
||||||
fun String.toLongEx(): Long = toLongOrNull() ?: 0
|
|
||||||
|
|
||||||
fun Context.listenForPackageChanges(onetime: Boolean = true, callback: () -> Unit) =
|
|
||||||
object : BroadcastReceiver() {
|
|
||||||
override fun onReceive(context: Context, intent: Intent) {
|
|
||||||
callback()
|
|
||||||
if (onetime) context.unregisterReceiver(this)
|
|
||||||
}
|
|
||||||
}.apply {
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
|
||||||
registerReceiver(this, IntentFilter().apply {
|
|
||||||
addAction(Intent.ACTION_PACKAGE_ADDED)
|
|
||||||
addAction(Intent.ACTION_PACKAGE_REMOVED)
|
|
||||||
addDataScheme("package")
|
|
||||||
}, Context.RECEIVER_EXPORTED)
|
|
||||||
} else {
|
|
||||||
registerReceiver(this, IntentFilter().apply {
|
|
||||||
addAction(Intent.ACTION_PACKAGE_ADDED)
|
|
||||||
addAction(Intent.ACTION_PACKAGE_REMOVED)
|
|
||||||
addDataScheme("package")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
inline fun <reified T : Serializable> Bundle.serializable(key: String): T? = when {
|
|
||||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU -> getSerializable(key, T::class.java)
|
|
||||||
else -> @Suppress("DEPRECATION") getSerializable(key) as? T
|
|
||||||
}
|
|
||||||
|
|
||||||
inline fun <reified T : Serializable> Intent.serializable(key: String): T? = when {
|
|
||||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU -> getSerializableExtra(key, T::class.java)
|
|
||||||
else -> @Suppress("DEPRECATION") getSerializableExtra(key) as? T
|
|
||||||
}
|
|
||||||
|
|
||||||
inline fun CharSequence?.isNotNullEmpty(): Boolean = (this != null && this.isNotEmpty())
|
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package com.v2ray.ang.extension
|
||||||
|
|
||||||
|
import android.preference.Preference
|
||||||
|
|
||||||
|
fun Preference.onClick(listener: () -> Unit) {
|
||||||
|
setOnPreferenceClickListener {
|
||||||
|
listener()
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
package com.v2ray.ang.fmt
|
|
||||||
|
|
||||||
import com.v2ray.ang.dto.EConfigType
|
|
||||||
import com.v2ray.ang.dto.ProfileItem
|
|
||||||
import com.v2ray.ang.dto.V2rayConfig
|
|
||||||
import com.v2ray.ang.util.JsonUtil
|
|
||||||
|
|
||||||
object CustomFmt : FmtBase() {
|
|
||||||
fun parse(str: String): ProfileItem? {
|
|
||||||
val config = ProfileItem.create(EConfigType.CUSTOM)
|
|
||||||
|
|
||||||
val fullConfig = JsonUtil.fromJson(str, V2rayConfig::class.java)
|
|
||||||
val outbound = fullConfig.getProxyOutbound()
|
|
||||||
|
|
||||||
config.remarks = fullConfig?.remarks ?: System.currentTimeMillis().toString()
|
|
||||||
config.server = outbound?.getServerAddress()
|
|
||||||
config.serverPort = outbound?.getServerPort().toString()
|
|
||||||
|
|
||||||
return config
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,85 +0,0 @@
|
|||||||
package com.v2ray.ang.fmt
|
|
||||||
|
|
||||||
import com.v2ray.ang.AppConfig
|
|
||||||
import com.v2ray.ang.dto.NetworkType
|
|
||||||
import com.v2ray.ang.dto.ProfileItem
|
|
||||||
import com.v2ray.ang.extension.isNotNullEmpty
|
|
||||||
import com.v2ray.ang.util.Utils
|
|
||||||
import java.net.URI
|
|
||||||
|
|
||||||
open class FmtBase {
|
|
||||||
fun toUri(config: ProfileItem, userInfo: String?, dicQuery: HashMap<String, String>?): String {
|
|
||||||
val query = if (dicQuery != null)
|
|
||||||
("?" + dicQuery.toList().joinToString(
|
|
||||||
separator = "&",
|
|
||||||
transform = { it.first + "=" + Utils.urlEncode(it.second) }))
|
|
||||||
else ""
|
|
||||||
|
|
||||||
val url = String.format(
|
|
||||||
"%s@%s:%s",
|
|
||||||
Utils.urlEncode(userInfo ?: ""),
|
|
||||||
Utils.getIpv6Address(config.server),
|
|
||||||
config.serverPort
|
|
||||||
)
|
|
||||||
|
|
||||||
return "${url}${query}#${Utils.urlEncode(config.remarks)}"
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getQueryParam(uri: URI): Map<String, String> {
|
|
||||||
return uri.rawQuery.split("&")
|
|
||||||
.associate { it.split("=").let { (k, v) -> k to Utils.urlDecode(v) } }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getQueryDic(config: ProfileItem): HashMap<String, String> {
|
|
||||||
val dicQuery = HashMap<String, String>()
|
|
||||||
dicQuery["security"] = config.security?.ifEmpty { "none" }.orEmpty()
|
|
||||||
config.sni.let { if (it.isNotNullEmpty()) dicQuery["sni"] = it.orEmpty() }
|
|
||||||
config.alpn.let { if (it.isNotNullEmpty()) dicQuery["alpn"] = it.orEmpty() }
|
|
||||||
config.fingerPrint.let { if (it.isNotNullEmpty()) dicQuery["fp"] = it.orEmpty() }
|
|
||||||
config.publicKey.let { if (it.isNotNullEmpty()) dicQuery["pbk"] = it.orEmpty() }
|
|
||||||
config.shortId.let { if (it.isNotNullEmpty()) dicQuery["sid"] = it.orEmpty() }
|
|
||||||
config.spiderX.let { if (it.isNotNullEmpty()) dicQuery["spx"] = it.orEmpty() }
|
|
||||||
config.flow.let { if (it.isNotNullEmpty()) dicQuery["flow"] = it.orEmpty() }
|
|
||||||
|
|
||||||
val networkType = NetworkType.fromString(config.network)
|
|
||||||
dicQuery["type"] = networkType.type
|
|
||||||
|
|
||||||
when (networkType) {
|
|
||||||
NetworkType.TCP -> {
|
|
||||||
dicQuery["headerType"] = config.headerType?.ifEmpty { "none" }.orEmpty()
|
|
||||||
config.host.let { if (it.isNotNullEmpty()) dicQuery["host"] = it.orEmpty() }
|
|
||||||
}
|
|
||||||
|
|
||||||
NetworkType.KCP -> {
|
|
||||||
dicQuery["headerType"] = config.headerType?.ifEmpty { "none" }.orEmpty()
|
|
||||||
config.seed.let { if (it.isNotNullEmpty()) dicQuery["seed"] = it.orEmpty() }
|
|
||||||
}
|
|
||||||
|
|
||||||
NetworkType.WS, NetworkType.HTTP_UPGRADE, NetworkType.SPLIT_HTTP -> {
|
|
||||||
config.host.let { if (it.isNotNullEmpty()) dicQuery["host"] = it.orEmpty() }
|
|
||||||
config.path.let { if (it.isNotNullEmpty()) dicQuery["path"] = it.orEmpty() }
|
|
||||||
}
|
|
||||||
|
|
||||||
NetworkType.HTTP, NetworkType.H2 -> {
|
|
||||||
dicQuery["type"] = "http"
|
|
||||||
config.host.let { if (it.isNotNullEmpty()) dicQuery["host"] = it.orEmpty() }
|
|
||||||
config.path.let { if (it.isNotNullEmpty()) dicQuery["path"] = it.orEmpty() }
|
|
||||||
}
|
|
||||||
|
|
||||||
NetworkType.QUIC -> {
|
|
||||||
dicQuery["headerType"] = config.headerType?.ifEmpty { "none" }.orEmpty()
|
|
||||||
config.quicSecurity.let { if (it.isNotNullEmpty()) dicQuery["quicSecurity"] = it.orEmpty() }
|
|
||||||
config.quicKey.let { if (it.isNotNullEmpty()) dicQuery["key"] = it.orEmpty() }
|
|
||||||
}
|
|
||||||
|
|
||||||
NetworkType.GRPC -> {
|
|
||||||
config.mode.let { if (it.isNotNullEmpty()) dicQuery["mode"] = it.orEmpty() }
|
|
||||||
config.authority.let { if (it.isNotNullEmpty()) dicQuery["authority"] = it.orEmpty() }
|
|
||||||
config.serviceName.let { if (it.isNotNullEmpty()) dicQuery["serviceName"] = it.orEmpty() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return dicQuery
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
package com.v2ray.ang.fmt
|
|
||||||
|
|
||||||
import com.v2ray.ang.dto.EConfigType
|
|
||||||
import com.v2ray.ang.dto.ProfileItem
|
|
||||||
import com.v2ray.ang.dto.V2rayConfig.OutboundBean
|
|
||||||
import com.v2ray.ang.extension.isNotNullEmpty
|
|
||||||
import kotlin.text.orEmpty
|
|
||||||
|
|
||||||
object HttpFmt : FmtBase() {
|
|
||||||
fun toOutbound(profileItem: ProfileItem): OutboundBean? {
|
|
||||||
val outboundBean = OutboundBean.create(EConfigType.HTTP)
|
|
||||||
|
|
||||||
outboundBean?.settings?.servers?.get(0)?.let { server ->
|
|
||||||
server.address = profileItem.server.orEmpty()
|
|
||||||
server.port = profileItem.serverPort.orEmpty().toInt()
|
|
||||||
if (profileItem.username.isNotNullEmpty()) {
|
|
||||||
val socksUsersBean = OutboundBean.OutSettingsBean.ServersBean.SocksUsersBean()
|
|
||||||
socksUsersBean.user = profileItem.username.orEmpty()
|
|
||||||
socksUsersBean.pass = profileItem.password.orEmpty()
|
|
||||||
server.users = listOf(socksUsersBean)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return outboundBean
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,120 +0,0 @@
|
|||||||
package com.v2ray.ang.fmt
|
|
||||||
|
|
||||||
import com.v2ray.ang.AppConfig
|
|
||||||
import com.v2ray.ang.AppConfig.LOOPBACK
|
|
||||||
import com.v2ray.ang.dto.EConfigType
|
|
||||||
import com.v2ray.ang.dto.Hysteria2Bean
|
|
||||||
import com.v2ray.ang.dto.ProfileItem
|
|
||||||
import com.v2ray.ang.dto.V2rayConfig.OutboundBean
|
|
||||||
import com.v2ray.ang.extension.idnHost
|
|
||||||
import com.v2ray.ang.extension.isNotNullEmpty
|
|
||||||
import com.v2ray.ang.handler.MmkvManager
|
|
||||||
import com.v2ray.ang.util.Utils
|
|
||||||
import java.net.URI
|
|
||||||
|
|
||||||
object Hysteria2Fmt : FmtBase() {
|
|
||||||
fun parse(str: String): ProfileItem? {
|
|
||||||
var allowInsecure = MmkvManager.decodeSettingsBool(AppConfig.PREF_ALLOW_INSECURE, false)
|
|
||||||
val config = ProfileItem.create(EConfigType.HYSTERIA2)
|
|
||||||
|
|
||||||
val uri = URI(Utils.fixIllegalUrl(str))
|
|
||||||
config.remarks = Utils.urlDecode(uri.fragment.orEmpty())
|
|
||||||
config.server = uri.idnHost
|
|
||||||
config.serverPort = uri.port.toString()
|
|
||||||
config.password = uri.userInfo
|
|
||||||
config.security = AppConfig.TLS
|
|
||||||
|
|
||||||
if (!uri.rawQuery.isNullOrEmpty()) {
|
|
||||||
val queryParam = getQueryParam(uri)
|
|
||||||
|
|
||||||
config.security = queryParam["security"] ?: AppConfig.TLS
|
|
||||||
config.insecure = if (queryParam["insecure"].isNullOrEmpty()) {
|
|
||||||
allowInsecure
|
|
||||||
} else {
|
|
||||||
queryParam["insecure"].orEmpty() == "1"
|
|
||||||
}
|
|
||||||
config.sni = queryParam["sni"]
|
|
||||||
config.alpn = queryParam["alpn"]
|
|
||||||
|
|
||||||
config.obfsPassword = queryParam["obfs-password"]
|
|
||||||
config.portHopping = queryParam["mport"]
|
|
||||||
config.pinSHA256 = queryParam["pinSHA256"]
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
return config
|
|
||||||
}
|
|
||||||
|
|
||||||
fun toUri(config: ProfileItem): String {
|
|
||||||
val dicQuery = HashMap<String, String>()
|
|
||||||
|
|
||||||
config.security.let { if (it != null) dicQuery["security"] = it }
|
|
||||||
config.sni.let { if (it.isNotNullEmpty()) dicQuery["sni"] = it.orEmpty() }
|
|
||||||
config.alpn.let { if (it.isNotNullEmpty()) dicQuery["alpn"] = it.orEmpty() }
|
|
||||||
config.insecure.let { dicQuery["insecure"] = if (it == true) "1" else "0" }
|
|
||||||
|
|
||||||
if (config.obfsPassword.isNotNullEmpty()) {
|
|
||||||
dicQuery["obfs"] = "salamander"
|
|
||||||
dicQuery["obfs-password"] = config.obfsPassword.orEmpty()
|
|
||||||
}
|
|
||||||
if (config.portHopping.isNotNullEmpty()) {
|
|
||||||
dicQuery["mport"] = config.portHopping.orEmpty()
|
|
||||||
}
|
|
||||||
if (config.pinSHA256.isNotNullEmpty()) {
|
|
||||||
dicQuery["pinSHA256"] = config.pinSHA256.orEmpty()
|
|
||||||
}
|
|
||||||
|
|
||||||
return toUri(config, config.password, dicQuery)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun toNativeConfig(config: ProfileItem, socksPort: Int): Hysteria2Bean? {
|
|
||||||
|
|
||||||
val obfs = if (config.obfsPassword.isNullOrEmpty()) null else
|
|
||||||
Hysteria2Bean.ObfsBean(
|
|
||||||
type = "salamander",
|
|
||||||
salamander = Hysteria2Bean.ObfsBean.SalamanderBean(
|
|
||||||
password = config.obfsPassword
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
val transport = if (config.portHopping.isNullOrEmpty()) null else
|
|
||||||
Hysteria2Bean.TransportBean(
|
|
||||||
type = "udp",
|
|
||||||
udp = Hysteria2Bean.TransportBean.TransportUdpBean(
|
|
||||||
hopInterval = (config.portHoppingInterval ?: "30") + "s"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
val server =
|
|
||||||
if (config.portHopping.isNullOrEmpty())
|
|
||||||
config.getServerAddressAndPort()
|
|
||||||
else
|
|
||||||
Utils.getIpv6Address(config.server) + ":" + config.portHopping
|
|
||||||
|
|
||||||
val bean = Hysteria2Bean(
|
|
||||||
server = server,
|
|
||||||
auth = config.password,
|
|
||||||
obfs = obfs,
|
|
||||||
transport = transport,
|
|
||||||
socks5 = Hysteria2Bean.Socks5Bean(
|
|
||||||
listen = "$LOOPBACK:${socksPort}",
|
|
||||||
),
|
|
||||||
http = Hysteria2Bean.Socks5Bean(
|
|
||||||
listen = "$LOOPBACK:${socksPort}",
|
|
||||||
),
|
|
||||||
tls = Hysteria2Bean.TlsBean(
|
|
||||||
sni = config.sni ?: config.server,
|
|
||||||
insecure = config.insecure,
|
|
||||||
pinSHA256 = if (config.pinSHA256.isNullOrEmpty()) null else config.pinSHA256
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return bean
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
fun toOutbound(profileItem: ProfileItem): OutboundBean? {
|
|
||||||
val outboundBean = OutboundBean.create(EConfigType.HYSTERIA2)
|
|
||||||
return outboundBean
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
package com.v2ray.ang.fmt
|
|
||||||
|
|
||||||
import com.v2ray.ang.dto.EConfigType
|
|
||||||
import com.v2ray.ang.dto.ProfileItem
|
|
||||||
import com.v2ray.ang.dto.V2rayConfig.OutboundBean
|
|
||||||
import com.v2ray.ang.extension.idnHost
|
|
||||||
import com.v2ray.ang.util.Utils
|
|
||||||
import java.net.URI
|
|
||||||
|
|
||||||
object ShadowsocksFmt : FmtBase() {
|
|
||||||
fun parse(str: String): ProfileItem? {
|
|
||||||
val config = ProfileItem.create(EConfigType.SHADOWSOCKS)
|
|
||||||
|
|
||||||
val uri = URI(Utils.fixIllegalUrl(str))
|
|
||||||
if (uri.idnHost.isEmpty() || uri.userInfo.isEmpty()) return null
|
|
||||||
|
|
||||||
config.remarks = Utils.urlDecode(uri.fragment.orEmpty())
|
|
||||||
config.server = uri.idnHost
|
|
||||||
config.serverPort = uri.port.toString()
|
|
||||||
|
|
||||||
val result = if (uri.userInfo.contains(":")) {
|
|
||||||
uri.userInfo.split(":", limit = 2)
|
|
||||||
} else {
|
|
||||||
Utils.decode(uri.userInfo).split(":", limit = 2)
|
|
||||||
}
|
|
||||||
if (result.count() == 2) {
|
|
||||||
config.method = result.first()
|
|
||||||
config.password = result.last()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!uri.rawQuery.isNullOrEmpty()) {
|
|
||||||
val queryParam = getQueryParam(uri)
|
|
||||||
|
|
||||||
if (queryParam["plugin"] == "obfs-local" && queryParam["obfs"] == "http") {
|
|
||||||
config.network = "tcp"
|
|
||||||
config.headerType = "http"
|
|
||||||
config.host = queryParam["obfs-host"]
|
|
||||||
config.path = queryParam["path"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return config
|
|
||||||
}
|
|
||||||
|
|
||||||
fun toUri(config: ProfileItem): String {
|
|
||||||
val pw = "${config.method}:${config.password}"
|
|
||||||
|
|
||||||
return toUri(config, pw, null)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun toOutbound(profileItem: ProfileItem): OutboundBean? {
|
|
||||||
val outboundBean = OutboundBean.create(EConfigType.SHADOWSOCKS)
|
|
||||||
|
|
||||||
outboundBean?.settings?.servers?.get(0)?.let { server ->
|
|
||||||
server.address = profileItem.server.orEmpty()
|
|
||||||
server.port = profileItem.serverPort.orEmpty().toInt()
|
|
||||||
server.password = profileItem.password
|
|
||||||
server.method = profileItem.method
|
|
||||||
}
|
|
||||||
|
|
||||||
return outboundBean
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
package com.v2ray.ang.fmt
|
|
||||||
|
|
||||||
import com.v2ray.ang.dto.EConfigType
|
|
||||||
import com.v2ray.ang.dto.ProfileItem
|
|
||||||
import com.v2ray.ang.dto.V2rayConfig.OutboundBean
|
|
||||||
import com.v2ray.ang.extension.idnHost
|
|
||||||
import com.v2ray.ang.extension.isNotNullEmpty
|
|
||||||
import com.v2ray.ang.util.Utils
|
|
||||||
import java.net.URI
|
|
||||||
import kotlin.text.orEmpty
|
|
||||||
|
|
||||||
object SocksFmt : FmtBase() {
|
|
||||||
fun parse(str: String): ProfileItem? {
|
|
||||||
val config = ProfileItem.create(EConfigType.SOCKS)
|
|
||||||
|
|
||||||
val uri = URI(Utils.fixIllegalUrl(str))
|
|
||||||
if (uri.idnHost.isEmpty()) return null
|
|
||||||
|
|
||||||
config.remarks = Utils.urlDecode(uri.fragment.orEmpty())
|
|
||||||
config.server = uri.idnHost
|
|
||||||
config.serverPort = uri.port.toString()
|
|
||||||
|
|
||||||
if (uri.userInfo?.isEmpty() == false) {
|
|
||||||
val result = Utils.decode(uri.userInfo).split(":", limit = 2)
|
|
||||||
if (result.count() == 2) {
|
|
||||||
config.username = result.first()
|
|
||||||
config.password = result.last()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return config
|
|
||||||
}
|
|
||||||
|
|
||||||
fun toUri(config: ProfileItem): String {
|
|
||||||
val pw =
|
|
||||||
if (config.username.isNotNullEmpty())
|
|
||||||
"${config.username}:${config.password}"
|
|
||||||
else
|
|
||||||
":"
|
|
||||||
|
|
||||||
return toUri(config, pw, null)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun toOutbound(profileItem: ProfileItem): OutboundBean? {
|
|
||||||
val outboundBean = OutboundBean.create(EConfigType.SOCKS)
|
|
||||||
|
|
||||||
outboundBean?.settings?.servers?.get(0)?.let { server ->
|
|
||||||
server.address = profileItem.server.orEmpty()
|
|
||||||
server.port = profileItem.serverPort.orEmpty().toInt()
|
|
||||||
if (profileItem.username.isNotNullEmpty()) {
|
|
||||||
val socksUsersBean = OutboundBean.OutSettingsBean.ServersBean.SocksUsersBean()
|
|
||||||
socksUsersBean.user = profileItem.username.orEmpty()
|
|
||||||
socksUsersBean.pass = profileItem.password.orEmpty()
|
|
||||||
server.users = listOf(socksUsersBean)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return outboundBean
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,103 +0,0 @@
|
|||||||
package com.v2ray.ang.fmt
|
|
||||||
|
|
||||||
import com.v2ray.ang.AppConfig
|
|
||||||
import com.v2ray.ang.dto.EConfigType
|
|
||||||
import com.v2ray.ang.dto.ProfileItem
|
|
||||||
import com.v2ray.ang.dto.V2rayConfig.OutboundBean
|
|
||||||
import com.v2ray.ang.extension.idnHost
|
|
||||||
import com.v2ray.ang.handler.MmkvManager
|
|
||||||
import com.v2ray.ang.util.Utils
|
|
||||||
import java.net.URI
|
|
||||||
import kotlin.text.orEmpty
|
|
||||||
|
|
||||||
object TrojanFmt : FmtBase() {
|
|
||||||
fun parse(str: String): ProfileItem? {
|
|
||||||
var allowInsecure = MmkvManager.decodeSettingsBool(AppConfig.PREF_ALLOW_INSECURE, false)
|
|
||||||
val config = ProfileItem.create(EConfigType.TROJAN)
|
|
||||||
|
|
||||||
val uri = URI(Utils.fixIllegalUrl(str))
|
|
||||||
config.remarks = Utils.urlDecode(uri.fragment.orEmpty())
|
|
||||||
config.server = uri.idnHost
|
|
||||||
config.serverPort = uri.port.toString()
|
|
||||||
config.password = uri.userInfo
|
|
||||||
|
|
||||||
if (uri.rawQuery.isNullOrEmpty()) {
|
|
||||||
config.security = AppConfig.TLS
|
|
||||||
config.insecure = allowInsecure
|
|
||||||
|
|
||||||
} else {
|
|
||||||
val queryParam = getQueryParam(uri)
|
|
||||||
|
|
||||||
config.network = queryParam["type"] ?: "tcp"
|
|
||||||
config.headerType = queryParam["headerType"]
|
|
||||||
config.host = queryParam["host"]
|
|
||||||
config.path = queryParam["path"]
|
|
||||||
|
|
||||||
config.seed = queryParam["seed"]
|
|
||||||
config.quicSecurity = queryParam["quicSecurity"]
|
|
||||||
config.quicKey = queryParam["key"]
|
|
||||||
config.mode = queryParam["mode"]
|
|
||||||
config.serviceName = queryParam["serviceName"]
|
|
||||||
config.authority = queryParam["authority"]
|
|
||||||
|
|
||||||
config.security = queryParam["security"]
|
|
||||||
config.insecure = if (queryParam["allowInsecure"].isNullOrEmpty()) {
|
|
||||||
allowInsecure
|
|
||||||
} else {
|
|
||||||
queryParam["allowInsecure"].orEmpty() == "1"
|
|
||||||
}
|
|
||||||
config.sni = queryParam["sni"]
|
|
||||||
config.fingerPrint = queryParam["fp"]
|
|
||||||
config.alpn = queryParam["alpn"]
|
|
||||||
config.publicKey = queryParam["pbk"]
|
|
||||||
config.shortId = queryParam["sid"]
|
|
||||||
config.spiderX = queryParam["spx"]
|
|
||||||
config.flow = queryParam["flow"]
|
|
||||||
}
|
|
||||||
|
|
||||||
return config
|
|
||||||
}
|
|
||||||
|
|
||||||
fun toUri(config: ProfileItem): String {
|
|
||||||
val dicQuery = getQueryDic(config)
|
|
||||||
|
|
||||||
return toUri(config, config.password, dicQuery)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun toOutbound(profileItem: ProfileItem): OutboundBean? {
|
|
||||||
val outboundBean = OutboundBean.create(EConfigType.TROJAN)
|
|
||||||
|
|
||||||
outboundBean?.settings?.servers?.get(0)?.let { server ->
|
|
||||||
server.address = profileItem.server.orEmpty()
|
|
||||||
server.port = profileItem.serverPort.orEmpty().toInt()
|
|
||||||
server.password = profileItem.password
|
|
||||||
server.flow = profileItem.flow
|
|
||||||
}
|
|
||||||
|
|
||||||
outboundBean?.streamSettings?.populateTransportSettings(
|
|
||||||
profileItem.network.orEmpty(),
|
|
||||||
profileItem.headerType,
|
|
||||||
profileItem.host,
|
|
||||||
profileItem.path,
|
|
||||||
profileItem.seed,
|
|
||||||
profileItem.quicSecurity,
|
|
||||||
profileItem.quicKey,
|
|
||||||
profileItem.mode,
|
|
||||||
profileItem.serviceName,
|
|
||||||
profileItem.authority,
|
|
||||||
)
|
|
||||||
|
|
||||||
outboundBean?.streamSettings?.populateTlsSettings(
|
|
||||||
profileItem.security.orEmpty(),
|
|
||||||
profileItem.insecure == true,
|
|
||||||
profileItem.sni,
|
|
||||||
profileItem.fingerPrint,
|
|
||||||
profileItem.alpn,
|
|
||||||
profileItem.publicKey,
|
|
||||||
profileItem.shortId,
|
|
||||||
profileItem.spiderX,
|
|
||||||
)
|
|
||||||
|
|
||||||
return outboundBean
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,104 +0,0 @@
|
|||||||
package com.v2ray.ang.fmt
|
|
||||||
|
|
||||||
import com.v2ray.ang.AppConfig
|
|
||||||
import com.v2ray.ang.dto.EConfigType
|
|
||||||
import com.v2ray.ang.dto.ProfileItem
|
|
||||||
import com.v2ray.ang.dto.V2rayConfig.OutboundBean
|
|
||||||
import com.v2ray.ang.extension.idnHost
|
|
||||||
import com.v2ray.ang.handler.MmkvManager
|
|
||||||
import com.v2ray.ang.util.Utils
|
|
||||||
import java.net.URI
|
|
||||||
|
|
||||||
object VlessFmt : FmtBase() {
|
|
||||||
|
|
||||||
fun parse(str: String): ProfileItem? {
|
|
||||||
var allowInsecure = MmkvManager.decodeSettingsBool(AppConfig.PREF_ALLOW_INSECURE, false)
|
|
||||||
val config = ProfileItem.create(EConfigType.VLESS)
|
|
||||||
|
|
||||||
val uri = URI(Utils.fixIllegalUrl(str))
|
|
||||||
if (uri.rawQuery.isNullOrEmpty()) return null
|
|
||||||
val queryParam = getQueryParam(uri)
|
|
||||||
|
|
||||||
config.remarks = Utils.urlDecode(uri.fragment.orEmpty())
|
|
||||||
config.server = uri.idnHost
|
|
||||||
config.serverPort = uri.port.toString()
|
|
||||||
config.password = uri.userInfo
|
|
||||||
config.method = queryParam["encryption"] ?: "none"
|
|
||||||
|
|
||||||
config.network = queryParam["type"] ?: "tcp"
|
|
||||||
config.headerType = queryParam["headerType"]
|
|
||||||
config.host = queryParam["host"]
|
|
||||||
config.path = queryParam["path"]
|
|
||||||
|
|
||||||
config.seed = queryParam["seed"]
|
|
||||||
config.quicSecurity = queryParam["quicSecurity"]
|
|
||||||
config.quicKey = queryParam["key"]
|
|
||||||
config.mode = queryParam["mode"]
|
|
||||||
config.serviceName = queryParam["serviceName"]
|
|
||||||
config.authority = queryParam["authority"]
|
|
||||||
|
|
||||||
config.security = queryParam["security"]
|
|
||||||
config.insecure = if (queryParam["allowInsecure"].isNullOrEmpty()) {
|
|
||||||
allowInsecure
|
|
||||||
} else {
|
|
||||||
queryParam["allowInsecure"].orEmpty() == "1"
|
|
||||||
}
|
|
||||||
config.sni = queryParam["sni"]
|
|
||||||
config.fingerPrint = queryParam["fp"]
|
|
||||||
config.alpn = queryParam["alpn"]
|
|
||||||
config.publicKey = queryParam["pbk"]
|
|
||||||
config.shortId = queryParam["sid"]
|
|
||||||
config.spiderX = queryParam["spx"]
|
|
||||||
config.flow = queryParam["flow"]
|
|
||||||
|
|
||||||
return config
|
|
||||||
}
|
|
||||||
|
|
||||||
fun toUri(config: ProfileItem): String {
|
|
||||||
val dicQuery = getQueryDic(config)
|
|
||||||
dicQuery["encryption"] = config.method ?: "none"
|
|
||||||
|
|
||||||
return toUri(config, config.password, dicQuery)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
fun toOutbound(profileItem: ProfileItem): OutboundBean? {
|
|
||||||
val outboundBean = OutboundBean.create(EConfigType.VLESS)
|
|
||||||
|
|
||||||
outboundBean?.settings?.vnext?.get(0)?.let { vnext ->
|
|
||||||
vnext.address = profileItem.server.orEmpty()
|
|
||||||
vnext.port = profileItem.serverPort.orEmpty().toInt()
|
|
||||||
vnext.users[0].id = profileItem.password.orEmpty()
|
|
||||||
vnext.users[0].encryption = profileItem.method
|
|
||||||
vnext.users[0].flow = profileItem.flow
|
|
||||||
}
|
|
||||||
|
|
||||||
outboundBean?.streamSettings?.populateTransportSettings(
|
|
||||||
profileItem.network.orEmpty(),
|
|
||||||
profileItem.headerType,
|
|
||||||
profileItem.host,
|
|
||||||
profileItem.path,
|
|
||||||
profileItem.seed,
|
|
||||||
profileItem.quicSecurity,
|
|
||||||
profileItem.quicKey,
|
|
||||||
profileItem.mode,
|
|
||||||
profileItem.serviceName,
|
|
||||||
profileItem.authority,
|
|
||||||
)
|
|
||||||
|
|
||||||
outboundBean?.streamSettings?.populateTlsSettings(
|
|
||||||
profileItem.security.orEmpty(),
|
|
||||||
profileItem.insecure == true,
|
|
||||||
profileItem.sni,
|
|
||||||
profileItem.fingerPrint,
|
|
||||||
profileItem.alpn,
|
|
||||||
profileItem.publicKey,
|
|
||||||
profileItem.shortId,
|
|
||||||
profileItem.spiderX,
|
|
||||||
)
|
|
||||||
|
|
||||||
return outboundBean
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,199 +0,0 @@
|
|||||||
package com.v2ray.ang.fmt
|
|
||||||
|
|
||||||
import android.text.TextUtils
|
|
||||||
import android.util.Log
|
|
||||||
import com.v2ray.ang.AppConfig
|
|
||||||
import com.v2ray.ang.dto.EConfigType
|
|
||||||
import com.v2ray.ang.dto.NetworkType
|
|
||||||
import com.v2ray.ang.dto.ProfileItem
|
|
||||||
import com.v2ray.ang.dto.V2rayConfig.OutboundBean
|
|
||||||
import com.v2ray.ang.dto.VmessQRCode
|
|
||||||
import com.v2ray.ang.extension.idnHost
|
|
||||||
import com.v2ray.ang.extension.isNotNullEmpty
|
|
||||||
import com.v2ray.ang.handler.MmkvManager
|
|
||||||
import com.v2ray.ang.util.JsonUtil
|
|
||||||
import com.v2ray.ang.util.Utils
|
|
||||||
import java.net.URI
|
|
||||||
import kotlin.text.orEmpty
|
|
||||||
|
|
||||||
object VmessFmt : FmtBase() {
|
|
||||||
fun parse(str: String): ProfileItem? {
|
|
||||||
if (str.indexOf('?') > 0 && str.indexOf('&') > 0) {
|
|
||||||
return parseVmessStd(str)
|
|
||||||
}
|
|
||||||
|
|
||||||
var allowInsecure = MmkvManager.decodeSettingsBool(AppConfig.PREF_ALLOW_INSECURE, false)
|
|
||||||
val config = ProfileItem.create(EConfigType.VMESS)
|
|
||||||
|
|
||||||
var result = str.replace(EConfigType.VMESS.protocolScheme, "")
|
|
||||||
result = Utils.decode(result)
|
|
||||||
if (TextUtils.isEmpty(result)) {
|
|
||||||
Log.d(AppConfig.ANG_PACKAGE, "R.string.toast_decoding_failed")
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
val vmessQRCode = JsonUtil.fromJson(result, VmessQRCode::class.java)
|
|
||||||
// Although VmessQRCode fields are non null, looks like Gson may still create null fields
|
|
||||||
if (TextUtils.isEmpty(vmessQRCode.add)
|
|
||||||
|| TextUtils.isEmpty(vmessQRCode.port)
|
|
||||||
|| TextUtils.isEmpty(vmessQRCode.id)
|
|
||||||
|| TextUtils.isEmpty(vmessQRCode.net)
|
|
||||||
) {
|
|
||||||
Log.d(AppConfig.ANG_PACKAGE, "R.string.toast_incorrect_protocol")
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
config.remarks = vmessQRCode.ps
|
|
||||||
config.server = vmessQRCode.add
|
|
||||||
config.serverPort = vmessQRCode.port
|
|
||||||
config.password = vmessQRCode.id
|
|
||||||
config.method = if (TextUtils.isEmpty(vmessQRCode.scy)) AppConfig.DEFAULT_SECURITY else vmessQRCode.scy
|
|
||||||
|
|
||||||
config.network = vmessQRCode.net ?: "tcp"
|
|
||||||
config.headerType = vmessQRCode.type
|
|
||||||
config.host = vmessQRCode.host
|
|
||||||
config.path = vmessQRCode.path
|
|
||||||
|
|
||||||
when (NetworkType.fromString(config.network)) {
|
|
||||||
NetworkType.KCP -> {
|
|
||||||
config.seed = vmessQRCode.path
|
|
||||||
}
|
|
||||||
|
|
||||||
NetworkType.QUIC -> {
|
|
||||||
config.quicSecurity = vmessQRCode.host
|
|
||||||
config.quicKey = vmessQRCode.path
|
|
||||||
}
|
|
||||||
|
|
||||||
NetworkType.GRPC -> {
|
|
||||||
config.mode = vmessQRCode.type
|
|
||||||
config.serviceName = vmessQRCode.path
|
|
||||||
config.authority = vmessQRCode.host
|
|
||||||
}
|
|
||||||
else -> {}
|
|
||||||
}
|
|
||||||
|
|
||||||
config.security = vmessQRCode.tls
|
|
||||||
config.insecure = allowInsecure
|
|
||||||
config.sni = vmessQRCode.sni
|
|
||||||
config.fingerPrint = vmessQRCode.fp
|
|
||||||
config.alpn = vmessQRCode.alpn
|
|
||||||
|
|
||||||
return config
|
|
||||||
}
|
|
||||||
|
|
||||||
fun toUri(config: ProfileItem): String {
|
|
||||||
val vmessQRCode = VmessQRCode()
|
|
||||||
|
|
||||||
vmessQRCode.v = "2"
|
|
||||||
vmessQRCode.ps = config.remarks
|
|
||||||
vmessQRCode.add = config.server.orEmpty()
|
|
||||||
vmessQRCode.port = config.serverPort.orEmpty()
|
|
||||||
vmessQRCode.id = config.password.orEmpty()
|
|
||||||
vmessQRCode.scy = config.method.orEmpty()
|
|
||||||
vmessQRCode.aid = "0"
|
|
||||||
|
|
||||||
vmessQRCode.net = config.network.orEmpty()
|
|
||||||
vmessQRCode.type = config.headerType.orEmpty()
|
|
||||||
when (NetworkType.fromString(config.network)) {
|
|
||||||
NetworkType.KCP -> {
|
|
||||||
vmessQRCode.path = config.seed.orEmpty()
|
|
||||||
}
|
|
||||||
|
|
||||||
NetworkType.QUIC -> {
|
|
||||||
vmessQRCode.host = config.quicSecurity.orEmpty()
|
|
||||||
vmessQRCode.path = config.quicKey.orEmpty()
|
|
||||||
}
|
|
||||||
|
|
||||||
NetworkType.GRPC -> {
|
|
||||||
vmessQRCode.type = config.mode.orEmpty()
|
|
||||||
vmessQRCode.path = config.serviceName.orEmpty()
|
|
||||||
vmessQRCode.host = config.authority.orEmpty()
|
|
||||||
}
|
|
||||||
else -> {}
|
|
||||||
}
|
|
||||||
|
|
||||||
config.host.let { if (it.isNotNullEmpty()) vmessQRCode.host = it.orEmpty() }
|
|
||||||
config.path.let { if (it.isNotNullEmpty()) vmessQRCode.path = it.orEmpty() }
|
|
||||||
|
|
||||||
vmessQRCode.tls = config.security.orEmpty()
|
|
||||||
vmessQRCode.sni = config.sni.orEmpty()
|
|
||||||
vmessQRCode.fp = config.fingerPrint.orEmpty()
|
|
||||||
vmessQRCode.alpn = config.alpn.orEmpty()
|
|
||||||
|
|
||||||
val json = JsonUtil.toJson(vmessQRCode)
|
|
||||||
return Utils.encode(json)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun parseVmessStd(str: String): ProfileItem? {
|
|
||||||
val allowInsecure = MmkvManager.decodeSettingsBool(AppConfig.PREF_ALLOW_INSECURE, false)
|
|
||||||
val config = ProfileItem.create(EConfigType.VMESS)
|
|
||||||
|
|
||||||
val uri = URI(Utils.fixIllegalUrl(str))
|
|
||||||
if (uri.rawQuery.isNullOrEmpty()) return null
|
|
||||||
val queryParam = getQueryParam(uri)
|
|
||||||
|
|
||||||
config.remarks = Utils.urlDecode(uri.fragment.orEmpty())
|
|
||||||
config.server = uri.idnHost
|
|
||||||
config.serverPort = uri.port.toString()
|
|
||||||
config.password = uri.userInfo
|
|
||||||
config.method = AppConfig.DEFAULT_SECURITY
|
|
||||||
|
|
||||||
config.network = NetworkType.fromString(queryParam["type"]).name
|
|
||||||
config.headerType = queryParam["headerType"]
|
|
||||||
config.host = queryParam["host"]
|
|
||||||
config.path = queryParam["path"]
|
|
||||||
|
|
||||||
config.seed = queryParam["seed"]
|
|
||||||
config.quicSecurity = queryParam["quicSecurity"]
|
|
||||||
config.quicKey = queryParam["key"]
|
|
||||||
config.mode = queryParam["mode"]
|
|
||||||
config.serviceName = queryParam["serviceName"]
|
|
||||||
config.authority = queryParam["authority"]
|
|
||||||
|
|
||||||
config.security = queryParam["security"]
|
|
||||||
config.insecure = if ((queryParam["allowInsecure"].orEmpty()) == "1") true else allowInsecure
|
|
||||||
config.sni = queryParam["sni"]
|
|
||||||
config.fingerPrint = queryParam["fp"]
|
|
||||||
config.alpn = queryParam["alpn"]
|
|
||||||
|
|
||||||
return config
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
fun toOutbound(profileItem: ProfileItem): OutboundBean? {
|
|
||||||
val outboundBean = OutboundBean.create(EConfigType.VMESS)
|
|
||||||
|
|
||||||
outboundBean?.settings?.vnext?.get(0)?.let { vnext ->
|
|
||||||
vnext.address = profileItem.server.orEmpty()
|
|
||||||
vnext.port = profileItem.serverPort.orEmpty().toInt()
|
|
||||||
vnext.users[0].id = profileItem.password.orEmpty()
|
|
||||||
vnext.users[0].security = profileItem.method
|
|
||||||
}
|
|
||||||
|
|
||||||
outboundBean?.streamSettings?.populateTransportSettings(
|
|
||||||
profileItem.network.orEmpty(),
|
|
||||||
profileItem.headerType,
|
|
||||||
profileItem.host,
|
|
||||||
profileItem.path,
|
|
||||||
profileItem.seed,
|
|
||||||
profileItem.quicSecurity,
|
|
||||||
profileItem.quicKey,
|
|
||||||
profileItem.mode,
|
|
||||||
profileItem.serviceName,
|
|
||||||
profileItem.authority,
|
|
||||||
)
|
|
||||||
|
|
||||||
outboundBean?.streamSettings?.populateTlsSettings(
|
|
||||||
profileItem.security.orEmpty(),
|
|
||||||
profileItem.insecure == true,
|
|
||||||
profileItem.sni,
|
|
||||||
profileItem.fingerPrint,
|
|
||||||
profileItem.alpn,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
null
|
|
||||||
)
|
|
||||||
|
|
||||||
return outboundBean
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,95 +0,0 @@
|
|||||||
package com.v2ray.ang.fmt
|
|
||||||
|
|
||||||
import com.v2ray.ang.AppConfig
|
|
||||||
import com.v2ray.ang.AppConfig.WIREGUARD_LOCAL_ADDRESS_V4
|
|
||||||
import com.v2ray.ang.dto.EConfigType
|
|
||||||
import com.v2ray.ang.dto.ProfileItem
|
|
||||||
import com.v2ray.ang.dto.V2rayConfig.OutboundBean
|
|
||||||
import com.v2ray.ang.extension.idnHost
|
|
||||||
import com.v2ray.ang.util.Utils
|
|
||||||
import java.net.URI
|
|
||||||
import kotlin.text.orEmpty
|
|
||||||
|
|
||||||
object WireguardFmt : FmtBase() {
|
|
||||||
fun parse(str: String): ProfileItem? {
|
|
||||||
val config = ProfileItem.create(EConfigType.WIREGUARD)
|
|
||||||
|
|
||||||
val uri = URI(Utils.fixIllegalUrl(str))
|
|
||||||
if (uri.rawQuery.isNullOrEmpty()) return null
|
|
||||||
val queryParam = getQueryParam(uri)
|
|
||||||
|
|
||||||
config.remarks = Utils.urlDecode(uri.fragment.orEmpty())
|
|
||||||
config.server = uri.idnHost
|
|
||||||
config.serverPort = uri.port.toString()
|
|
||||||
|
|
||||||
config.secretKey = uri.userInfo
|
|
||||||
config.localAddress = (queryParam["address"] ?: WIREGUARD_LOCAL_ADDRESS_V4)
|
|
||||||
config.publicKey = queryParam["publickey"].orEmpty()
|
|
||||||
config.mtu = Utils.parseInt(queryParam["mtu"] ?: AppConfig.WIREGUARD_LOCAL_MTU)
|
|
||||||
config.reserved = (queryParam["reserved"] ?: "0,0,0")
|
|
||||||
|
|
||||||
return config
|
|
||||||
}
|
|
||||||
|
|
||||||
fun parseWireguardConfFile(str: String): ProfileItem? {
|
|
||||||
val config = ProfileItem.create(EConfigType.WIREGUARD)
|
|
||||||
val queryParam: MutableMap<String, String> = mutableMapOf()
|
|
||||||
|
|
||||||
var currentSection: String? = null
|
|
||||||
|
|
||||||
str.lines().forEach { line ->
|
|
||||||
val trimmedLine = line.trim()
|
|
||||||
|
|
||||||
when {
|
|
||||||
trimmedLine.startsWith("[Interface]", ignoreCase = true) -> currentSection = "Interface"
|
|
||||||
trimmedLine.startsWith("[Peer]", ignoreCase = true) -> currentSection = "Peer"
|
|
||||||
trimmedLine.isBlank() || trimmedLine.startsWith("#") -> Unit // Skip blank lines or comments
|
|
||||||
currentSection != null -> {
|
|
||||||
val (key, value) = trimmedLine.split("=").map { it.trim() }
|
|
||||||
queryParam[key.lowercase()] = value // Store the key in lowercase for case-insensitivity
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
config.secretKey = queryParam["privatekey"].orEmpty()
|
|
||||||
config.localAddress = (queryParam["address"] ?: WIREGUARD_LOCAL_ADDRESS_V4)
|
|
||||||
config.publicKey = queryParam["publickey"].orEmpty()
|
|
||||||
config.mtu = Utils.parseInt(queryParam["mtu"] ?: AppConfig.WIREGUARD_LOCAL_MTU)
|
|
||||||
config.reserved = (queryParam["reserved"] ?: "0,0,0")
|
|
||||||
|
|
||||||
return config
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
fun toUri(config: ProfileItem): String {
|
|
||||||
val dicQuery = HashMap<String, String>()
|
|
||||||
|
|
||||||
dicQuery["publickey"] = config.publicKey.orEmpty()
|
|
||||||
if (config.reserved != null) {
|
|
||||||
dicQuery["reserved"] = Utils.removeWhiteSpace(config.reserved).orEmpty()
|
|
||||||
}
|
|
||||||
dicQuery["address"] = Utils.removeWhiteSpace(config.localAddress).orEmpty()
|
|
||||||
if (config.mtu != null) {
|
|
||||||
dicQuery["mtu"] = config.mtu.toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
return toUri(config, config.secretKey, dicQuery)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun toOutbound(profileItem: ProfileItem): OutboundBean? {
|
|
||||||
val outboundBean = OutboundBean.create(EConfigType.WIREGUARD)
|
|
||||||
|
|
||||||
outboundBean?.settings?.let { wireguard ->
|
|
||||||
wireguard.secretKey = profileItem.secretKey
|
|
||||||
wireguard.address = (profileItem.localAddress ?: WIREGUARD_LOCAL_ADDRESS_V4).split(",")
|
|
||||||
wireguard.peers?.get(0)?.publicKey = profileItem.publicKey.orEmpty()
|
|
||||||
wireguard.peers?.get(0)?.endpoint = Utils.getIpv6Address(profileItem.server) + ":${profileItem.serverPort}"
|
|
||||||
wireguard.mtu = profileItem.mtu?.toInt()
|
|
||||||
wireguard.reserved = profileItem.reserved?.split(",")?.map { it.toInt() }
|
|
||||||
}
|
|
||||||
|
|
||||||
return outboundBean
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,399 +0,0 @@
|
|||||||
package com.v2ray.ang.handler
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.graphics.Bitmap
|
|
||||||
import android.text.TextUtils
|
|
||||||
import android.util.Log
|
|
||||||
import com.v2ray.ang.AppConfig
|
|
||||||
import com.v2ray.ang.AppConfig.HY2
|
|
||||||
import com.v2ray.ang.R
|
|
||||||
import com.v2ray.ang.dto.*
|
|
||||||
import com.v2ray.ang.fmt.CustomFmt
|
|
||||||
import com.v2ray.ang.fmt.Hysteria2Fmt
|
|
||||||
import com.v2ray.ang.fmt.ShadowsocksFmt
|
|
||||||
import com.v2ray.ang.fmt.SocksFmt
|
|
||||||
import com.v2ray.ang.fmt.TrojanFmt
|
|
||||||
import com.v2ray.ang.fmt.VlessFmt
|
|
||||||
import com.v2ray.ang.fmt.VmessFmt
|
|
||||||
import com.v2ray.ang.fmt.WireguardFmt
|
|
||||||
import com.v2ray.ang.util.JsonUtil
|
|
||||||
import com.v2ray.ang.util.QRCodeDecoder
|
|
||||||
import com.v2ray.ang.util.Utils
|
|
||||||
import java.net.URI
|
|
||||||
|
|
||||||
object AngConfigManager {
|
|
||||||
/**
|
|
||||||
* parse config form qrcode or...
|
|
||||||
*/
|
|
||||||
private fun parseConfig(
|
|
||||||
str: String?,
|
|
||||||
subid: String,
|
|
||||||
subItem: SubscriptionItem?,
|
|
||||||
removedSelectedServer: ProfileItem?
|
|
||||||
): Int {
|
|
||||||
try {
|
|
||||||
if (str == null || TextUtils.isEmpty(str)) {
|
|
||||||
return R.string.toast_none_data
|
|
||||||
}
|
|
||||||
|
|
||||||
val config = if (str.startsWith(EConfigType.VMESS.protocolScheme)) {
|
|
||||||
VmessFmt.parse(str)
|
|
||||||
} else if (str.startsWith(EConfigType.SHADOWSOCKS.protocolScheme)) {
|
|
||||||
ShadowsocksFmt.parse(str)
|
|
||||||
} else if (str.startsWith(EConfigType.SOCKS.protocolScheme)) {
|
|
||||||
SocksFmt.parse(str)
|
|
||||||
} else if (str.startsWith(EConfigType.TROJAN.protocolScheme)) {
|
|
||||||
TrojanFmt.parse(str)
|
|
||||||
} else if (str.startsWith(EConfigType.VLESS.protocolScheme)) {
|
|
||||||
VlessFmt.parse(str)
|
|
||||||
} else if (str.startsWith(EConfigType.WIREGUARD.protocolScheme)) {
|
|
||||||
WireguardFmt.parse(str)
|
|
||||||
} else if (str.startsWith(EConfigType.HYSTERIA2.protocolScheme) || str.startsWith(HY2)) {
|
|
||||||
Hysteria2Fmt.parse(str)
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
if (config == null) {
|
|
||||||
return R.string.toast_incorrect_protocol
|
|
||||||
}
|
|
||||||
//filter
|
|
||||||
if (subItem?.filter != null && subItem.filter?.isNotEmpty() == true && config.remarks.isNotEmpty()) {
|
|
||||||
val matched = Regex(pattern = subItem.filter ?: "")
|
|
||||||
.containsMatchIn(input = config.remarks)
|
|
||||||
if (!matched) return -1
|
|
||||||
}
|
|
||||||
|
|
||||||
config.subscriptionId = subid
|
|
||||||
val guid = MmkvManager.encodeServerConfig("", config)
|
|
||||||
if (removedSelectedServer != null &&
|
|
||||||
config.server == removedSelectedServer.server && config.serverPort == removedSelectedServer.serverPort
|
|
||||||
) {
|
|
||||||
MmkvManager.setSelectServer(guid)
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
e.printStackTrace()
|
|
||||||
return -1
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* share config
|
|
||||||
*/
|
|
||||||
private fun shareConfig(guid: String): String {
|
|
||||||
try {
|
|
||||||
val config = MmkvManager.decodeServerConfig(guid) ?: return ""
|
|
||||||
|
|
||||||
return config.configType.protocolScheme + when (config.configType) {
|
|
||||||
EConfigType.VMESS -> VmessFmt.toUri(config)
|
|
||||||
EConfigType.CUSTOM -> ""
|
|
||||||
EConfigType.SHADOWSOCKS -> ShadowsocksFmt.toUri(config)
|
|
||||||
EConfigType.SOCKS -> SocksFmt.toUri(config)
|
|
||||||
EConfigType.HTTP -> ""
|
|
||||||
EConfigType.VLESS -> VlessFmt.toUri(config)
|
|
||||||
EConfigType.TROJAN -> TrojanFmt.toUri(config)
|
|
||||||
EConfigType.WIREGUARD -> WireguardFmt.toUri(config)
|
|
||||||
EConfigType.HYSTERIA2 -> Hysteria2Fmt.toUri(config)
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
e.printStackTrace()
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* share2Clipboard
|
|
||||||
*/
|
|
||||||
fun share2Clipboard(context: Context, guid: String): Int {
|
|
||||||
try {
|
|
||||||
val conf = shareConfig(guid)
|
|
||||||
if (TextUtils.isEmpty(conf)) {
|
|
||||||
return -1
|
|
||||||
}
|
|
||||||
|
|
||||||
Utils.setClipboard(context, conf)
|
|
||||||
|
|
||||||
} catch (e: Exception) {
|
|
||||||
e.printStackTrace()
|
|
||||||
return -1
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* share2Clipboard
|
|
||||||
*/
|
|
||||||
fun shareNonCustomConfigsToClipboard(context: Context, serverList: List<String>): Int {
|
|
||||||
try {
|
|
||||||
val sb = StringBuilder()
|
|
||||||
for (guid in serverList) {
|
|
||||||
val url = shareConfig(guid)
|
|
||||||
if (TextUtils.isEmpty(url)) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
sb.append(url)
|
|
||||||
sb.appendLine()
|
|
||||||
}
|
|
||||||
if (sb.count() > 0) {
|
|
||||||
Utils.setClipboard(context, sb.toString())
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
e.printStackTrace()
|
|
||||||
return -1
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* share2QRCode
|
|
||||||
*/
|
|
||||||
fun share2QRCode(guid: String): Bitmap? {
|
|
||||||
try {
|
|
||||||
val conf = shareConfig(guid)
|
|
||||||
if (TextUtils.isEmpty(conf)) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
return QRCodeDecoder.createQRCode(conf)
|
|
||||||
|
|
||||||
} catch (e: Exception) {
|
|
||||||
e.printStackTrace()
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* shareFullContent2Clipboard
|
|
||||||
*/
|
|
||||||
fun shareFullContent2Clipboard(context: Context, guid: String?): Int {
|
|
||||||
try {
|
|
||||||
if (guid == null) return -1
|
|
||||||
val result = V2rayConfigManager.getV2rayConfig(context, guid)
|
|
||||||
if (result.status) {
|
|
||||||
Utils.setClipboard(context, result.content)
|
|
||||||
} else {
|
|
||||||
return -1
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
e.printStackTrace()
|
|
||||||
return -1
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
fun importBatchConfig(server: String?, subid: String, append: Boolean): Pair<Int, Int> {
|
|
||||||
var count = parseBatchConfig(Utils.decode(server), subid, append)
|
|
||||||
if (count <= 0) {
|
|
||||||
count = parseBatchConfig(server, subid, append)
|
|
||||||
}
|
|
||||||
if (count <= 0) {
|
|
||||||
count = parseCustomConfigServer(server, subid)
|
|
||||||
}
|
|
||||||
|
|
||||||
var countSub = parseBatchSubscription(server)
|
|
||||||
if (countSub <= 0) {
|
|
||||||
countSub = parseBatchSubscription(Utils.decode(server))
|
|
||||||
}
|
|
||||||
if (countSub > 0) {
|
|
||||||
updateConfigViaSubAll()
|
|
||||||
}
|
|
||||||
|
|
||||||
return count to countSub
|
|
||||||
}
|
|
||||||
|
|
||||||
fun parseBatchSubscription(servers: String?): Int {
|
|
||||||
try {
|
|
||||||
if (servers == null) {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
var count = 0
|
|
||||||
servers.lines()
|
|
||||||
.distinct()
|
|
||||||
.forEach { str ->
|
|
||||||
if (Utils.isValidSubUrl(str)) {
|
|
||||||
count += importUrlAsSubscription(str)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return count
|
|
||||||
} catch (e: Exception) {
|
|
||||||
e.printStackTrace()
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
fun parseBatchConfig(servers: String?, subid: String, append: Boolean): Int {
|
|
||||||
try {
|
|
||||||
if (servers == null) {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
val removedSelectedServer =
|
|
||||||
if (!TextUtils.isEmpty(subid) && !append) {
|
|
||||||
MmkvManager.decodeServerConfig(
|
|
||||||
MmkvManager.getSelectServer().orEmpty()
|
|
||||||
)?.let {
|
|
||||||
if (it.subscriptionId == subid) {
|
|
||||||
return@let it
|
|
||||||
}
|
|
||||||
return@let null
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
if (!append) {
|
|
||||||
MmkvManager.removeServerViaSubid(subid)
|
|
||||||
}
|
|
||||||
|
|
||||||
val subItem = MmkvManager.decodeSubscription(subid)
|
|
||||||
var count = 0
|
|
||||||
servers.lines()
|
|
||||||
.distinct()
|
|
||||||
.reversed()
|
|
||||||
.forEach {
|
|
||||||
val resId = parseConfig(it, subid, subItem, removedSelectedServer)
|
|
||||||
if (resId == 0) {
|
|
||||||
count++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return count
|
|
||||||
} catch (e: Exception) {
|
|
||||||
e.printStackTrace()
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
fun parseCustomConfigServer(server: String?, subid: String): Int {
|
|
||||||
if (server == null) {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
if (server.contains("inbounds")
|
|
||||||
&& server.contains("outbounds")
|
|
||||||
&& server.contains("routing")
|
|
||||||
) {
|
|
||||||
try {
|
|
||||||
val serverList: Array<Any> =
|
|
||||||
JsonUtil.fromJson(server, Array<Any>::class.java)
|
|
||||||
|
|
||||||
if (serverList.isNotEmpty()) {
|
|
||||||
var count = 0
|
|
||||||
for (srv in serverList.reversed()) {
|
|
||||||
val config = CustomFmt.parse(JsonUtil.toJson(srv)) ?: continue
|
|
||||||
config.subscriptionId = subid
|
|
||||||
val key = MmkvManager.encodeServerConfig("", config)
|
|
||||||
MmkvManager.encodeServerRaw(key, JsonUtil.toJsonPretty(srv))
|
|
||||||
count += 1
|
|
||||||
}
|
|
||||||
return count
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
e.printStackTrace()
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// For compatibility
|
|
||||||
val config = CustomFmt.parse(server) ?: return 0
|
|
||||||
config.subscriptionId = subid
|
|
||||||
val key = MmkvManager.encodeServerConfig("", config)
|
|
||||||
MmkvManager.encodeServerRaw(key, server)
|
|
||||||
return 1
|
|
||||||
} catch (e: Exception) {
|
|
||||||
e.printStackTrace()
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
} else if (server.startsWith("[Interface]") && server.contains("[Peer]")) {
|
|
||||||
try {
|
|
||||||
val config = WireguardFmt.parseWireguardConfFile(server) ?: return R.string.toast_incorrect_protocol
|
|
||||||
val key = MmkvManager.encodeServerConfig("", config)
|
|
||||||
MmkvManager.encodeServerRaw(key, server)
|
|
||||||
return 1
|
|
||||||
} catch (e: Exception) {
|
|
||||||
e.printStackTrace()
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
} else {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun updateConfigViaSubAll(): Int {
|
|
||||||
var count = 0
|
|
||||||
try {
|
|
||||||
MmkvManager.decodeSubscriptions().forEach {
|
|
||||||
count += updateConfigViaSub(it)
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
e.printStackTrace()
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
return count
|
|
||||||
}
|
|
||||||
|
|
||||||
fun updateConfigViaSub(it: Pair<String, SubscriptionItem>): Int {
|
|
||||||
try {
|
|
||||||
if (TextUtils.isEmpty(it.first)
|
|
||||||
|| TextUtils.isEmpty(it.second.remarks)
|
|
||||||
|| TextUtils.isEmpty(it.second.url)
|
|
||||||
) {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
if (!it.second.enabled) {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
val url = Utils.idnToASCII(it.second.url)
|
|
||||||
if (!Utils.isValidUrl(url)) {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
Log.d(AppConfig.ANG_PACKAGE, url)
|
|
||||||
|
|
||||||
var configText = try {
|
|
||||||
val httpPort = SettingsManager.getHttpPort()
|
|
||||||
Utils.getUrlContentWithCustomUserAgent(url, 30000, httpPort)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(AppConfig.ANG_PACKAGE, "Update subscription: proxy not ready or other error, try……")
|
|
||||||
//e.printStackTrace()
|
|
||||||
""
|
|
||||||
}
|
|
||||||
if (configText.isEmpty()) {
|
|
||||||
configText = try {
|
|
||||||
Utils.getUrlContentWithCustomUserAgent(url)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
e.printStackTrace()
|
|
||||||
""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (configText.isEmpty()) {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
return parseConfigViaSub(configText, it.first, false)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
e.printStackTrace()
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun parseConfigViaSub(server: String?, subid: String, append: Boolean): Int {
|
|
||||||
var count = parseBatchConfig(Utils.decode(server), subid, append)
|
|
||||||
if (count <= 0) {
|
|
||||||
count = parseBatchConfig(server, subid, append)
|
|
||||||
}
|
|
||||||
if (count <= 0) {
|
|
||||||
count = parseCustomConfigServer(server, subid)
|
|
||||||
}
|
|
||||||
return count
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun importUrlAsSubscription(url: String): Int {
|
|
||||||
val subscriptions = MmkvManager.decodeSubscriptions()
|
|
||||||
subscriptions.forEach {
|
|
||||||
if (it.second.url == url) {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val uri = URI(Utils.fixIllegalUrl(url))
|
|
||||||
val subItem = SubscriptionItem()
|
|
||||||
subItem.remarks = uri.fragment ?: "import sub"
|
|
||||||
subItem.url = url
|
|
||||||
MmkvManager.encodeSubscription("", subItem)
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,190 +0,0 @@
|
|||||||
package com.v2ray.ang.handler
|
|
||||||
|
|
||||||
import android.util.Log
|
|
||||||
import com.tencent.mmkv.MMKV
|
|
||||||
import com.v2ray.ang.AppConfig
|
|
||||||
import com.v2ray.ang.AppConfig.ANG_PACKAGE
|
|
||||||
import com.v2ray.ang.dto.EConfigType
|
|
||||||
import com.v2ray.ang.dto.ProfileItem
|
|
||||||
import com.v2ray.ang.dto.ServerConfig
|
|
||||||
import com.v2ray.ang.handler.MmkvManager.decodeServerConfig
|
|
||||||
import com.v2ray.ang.util.JsonUtil
|
|
||||||
import com.v2ray.ang.util.Utils
|
|
||||||
|
|
||||||
object MigrateManager {
|
|
||||||
private const val ID_SERVER_CONFIG = "SERVER_CONFIG"
|
|
||||||
private val serverStorage by lazy { MMKV.mmkvWithID(ID_SERVER_CONFIG, MMKV.MULTI_PROCESS_MODE) }
|
|
||||||
|
|
||||||
fun migrateServerConfig2Profile(): Boolean {
|
|
||||||
if (serverStorage.count().toInt() == 0) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
val serverList = serverStorage.allKeys() ?: return false
|
|
||||||
Log.d(ANG_PACKAGE, "migrateServerConfig2Profile-" + serverList.count())
|
|
||||||
|
|
||||||
for (guid in serverList) {
|
|
||||||
var configOld = decodeServerConfigOld(guid) ?: continue
|
|
||||||
var config = decodeServerConfig(guid)
|
|
||||||
if (config != null) {
|
|
||||||
serverStorage.remove(guid)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
config = migrateServerConfig2ProfileSub(configOld) ?: continue
|
|
||||||
config.subscriptionId = configOld.subscriptionId
|
|
||||||
|
|
||||||
MmkvManager.encodeServerConfig(guid, config)
|
|
||||||
|
|
||||||
//check and remove old
|
|
||||||
decodeServerConfig(guid) ?: continue
|
|
||||||
//serverStorage.remove(guid)
|
|
||||||
Log.d(ANG_PACKAGE, "migrateServerConfig2Profile-" + config.remarks)
|
|
||||||
}
|
|
||||||
Log.d(ANG_PACKAGE, "migrateServerConfig2Profile-end")
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun migrateServerConfig2ProfileSub(configOld: ServerConfig): ProfileItem? {
|
|
||||||
return when (configOld.getProxyOutbound()?.protocol) {
|
|
||||||
EConfigType.VMESS.name.lowercase() -> migrate2ProfileCommon(configOld)
|
|
||||||
EConfigType.VLESS.name.lowercase() -> migrate2ProfileCommon(configOld)
|
|
||||||
EConfigType.TROJAN.name.lowercase() -> migrate2ProfileCommon(configOld)
|
|
||||||
EConfigType.SHADOWSOCKS.name.lowercase() -> migrate2ProfileCommon(configOld)
|
|
||||||
|
|
||||||
EConfigType.SOCKS.name.lowercase() -> migrate2ProfileSocks(configOld)
|
|
||||||
EConfigType.HTTP.name.lowercase() -> migrate2ProfileHttp(configOld)
|
|
||||||
EConfigType.WIREGUARD.name.lowercase() -> migrate2ProfileWireguard(configOld)
|
|
||||||
EConfigType.HYSTERIA2.name.lowercase() -> migrate2ProfileHysteria2(configOld)
|
|
||||||
|
|
||||||
EConfigType.CUSTOM.name.lowercase() -> migrate2ProfileCustom(configOld)
|
|
||||||
|
|
||||||
else -> null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun migrate2ProfileCommon(configOld: ServerConfig): ProfileItem? {
|
|
||||||
val config = ProfileItem.create(configOld.configType)
|
|
||||||
|
|
||||||
val outbound = configOld.getProxyOutbound() ?: return null
|
|
||||||
config.remarks = configOld.remarks
|
|
||||||
config.server = outbound.getServerAddress()
|
|
||||||
config.serverPort = outbound.getServerPort().toString()
|
|
||||||
config.method = outbound.getSecurityEncryption()
|
|
||||||
config.password = outbound.getPassword()
|
|
||||||
config.flow = outbound?.settings?.vnext?.get(0)?.users?.get(0)?.flow ?: outbound?.settings?.servers?.get(0)?.flow
|
|
||||||
|
|
||||||
config.network = outbound?.streamSettings?.network ?: "tcp"
|
|
||||||
outbound.getTransportSettingDetails()?.let { transportDetails ->
|
|
||||||
config.headerType = transportDetails[0].orEmpty()
|
|
||||||
config.host = transportDetails[1].orEmpty()
|
|
||||||
config.path = transportDetails[2].orEmpty()
|
|
||||||
}
|
|
||||||
|
|
||||||
config.seed = outbound?.streamSettings?.kcpSettings?.seed
|
|
||||||
config.quicSecurity = outbound?.streamSettings?.quicSettings?.security
|
|
||||||
config.quicKey = outbound?.streamSettings?.quicSettings?.key
|
|
||||||
config.mode = if (outbound?.streamSettings?.grpcSettings?.multiMode == true) "multi" else "gun"
|
|
||||||
config.serviceName = outbound?.streamSettings?.grpcSettings?.serviceName
|
|
||||||
config.authority = outbound?.streamSettings?.grpcSettings?.authority
|
|
||||||
|
|
||||||
config.security = outbound.streamSettings?.security
|
|
||||||
val tlsSettings = outbound?.streamSettings?.realitySettings ?: outbound?.streamSettings?.tlsSettings
|
|
||||||
config.insecure = tlsSettings?.allowInsecure
|
|
||||||
config.sni = tlsSettings?.serverName
|
|
||||||
config.fingerPrint = tlsSettings?.fingerprint
|
|
||||||
config.alpn = Utils.removeWhiteSpace(tlsSettings?.alpn?.joinToString(",")).toString()
|
|
||||||
|
|
||||||
config.publicKey = tlsSettings?.publicKey
|
|
||||||
config.shortId = tlsSettings?.shortId
|
|
||||||
config.spiderX = tlsSettings?.spiderX
|
|
||||||
|
|
||||||
return config
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun migrate2ProfileSocks(configOld: ServerConfig): ProfileItem? {
|
|
||||||
val config = ProfileItem.create(EConfigType.SOCKS)
|
|
||||||
|
|
||||||
val outbound = configOld.getProxyOutbound() ?: return null
|
|
||||||
config.remarks = configOld.remarks
|
|
||||||
config.server = outbound.getServerAddress()
|
|
||||||
config.serverPort = outbound.getServerPort().toString()
|
|
||||||
config.username = outbound.settings?.servers?.get(0)?.users?.get(0)?.user
|
|
||||||
config.password = outbound.getPassword()
|
|
||||||
|
|
||||||
return config
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun migrate2ProfileHttp(configOld: ServerConfig): ProfileItem? {
|
|
||||||
val config = ProfileItem.create(EConfigType.HTTP)
|
|
||||||
|
|
||||||
val outbound = configOld.getProxyOutbound() ?: return null
|
|
||||||
config.remarks = configOld.remarks
|
|
||||||
config.server = outbound.getServerAddress()
|
|
||||||
config.serverPort = outbound.getServerPort().toString()
|
|
||||||
config.username = outbound.settings?.servers?.get(0)?.users?.get(0)?.user
|
|
||||||
config.password = outbound.getPassword()
|
|
||||||
|
|
||||||
return config
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun migrate2ProfileWireguard(configOld: ServerConfig): ProfileItem? {
|
|
||||||
val config = ProfileItem.create(EConfigType.WIREGUARD)
|
|
||||||
|
|
||||||
val outbound = configOld.getProxyOutbound() ?: return null
|
|
||||||
config.remarks = configOld.remarks
|
|
||||||
config.server = outbound.getServerAddress()
|
|
||||||
config.serverPort = outbound.getServerPort().toString()
|
|
||||||
|
|
||||||
outbound.settings?.let { wireguard ->
|
|
||||||
config.secretKey = wireguard.secretKey
|
|
||||||
config.localAddress = Utils.removeWhiteSpace((wireguard.address as List<*>).joinToString(",")).toString()
|
|
||||||
config.publicKey = wireguard.peers?.getOrNull(0)?.publicKey
|
|
||||||
config.mtu = wireguard.mtu
|
|
||||||
config.reserved = Utils.removeWhiteSpace(wireguard.reserved?.joinToString(",")).toString()
|
|
||||||
}
|
|
||||||
return config
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun migrate2ProfileHysteria2(configOld: ServerConfig): ProfileItem? {
|
|
||||||
val config = ProfileItem.create(EConfigType.HYSTERIA2)
|
|
||||||
|
|
||||||
val outbound = configOld.getProxyOutbound() ?: return null
|
|
||||||
config.remarks = configOld.remarks
|
|
||||||
config.server = outbound.getServerAddress()
|
|
||||||
config.serverPort = outbound.getServerPort().toString()
|
|
||||||
config.password = outbound.getPassword()
|
|
||||||
|
|
||||||
config.security = AppConfig.TLS
|
|
||||||
outbound.streamSettings?.tlsSettings?.let { tlsSetting ->
|
|
||||||
config.insecure = tlsSetting.allowInsecure
|
|
||||||
config.sni = tlsSetting.serverName
|
|
||||||
config.alpn = Utils.removeWhiteSpace(tlsSetting.alpn?.joinToString(",")).orEmpty()
|
|
||||||
|
|
||||||
}
|
|
||||||
config.obfsPassword = outbound.settings?.obfsPassword
|
|
||||||
|
|
||||||
return config
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun migrate2ProfileCustom(configOld: ServerConfig): ProfileItem? {
|
|
||||||
val config = ProfileItem.create(EConfigType.CUSTOM)
|
|
||||||
|
|
||||||
val outbound = configOld.getProxyOutbound() ?: return null
|
|
||||||
config.remarks = configOld.remarks
|
|
||||||
config.server = outbound.getServerAddress()
|
|
||||||
config.serverPort = outbound.getServerPort().toString()
|
|
||||||
|
|
||||||
return config
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private fun decodeServerConfigOld(guid: String): ServerConfig? {
|
|
||||||
if (guid.isBlank()) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
val json = serverStorage.decodeString(guid)
|
|
||||||
if (json.isNullOrBlank()) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
return JsonUtil.fromJson(json, ServerConfig::class.java)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user