Unverified Commit ec3a3054 authored by Max Lv's avatar Max Lv Committed by GitHub

Merge pull request #1478 from Mygod/gradle-kotlin

Porting this project to Gradle + Kotlin
parents b4f13ed0 efae2f02
target # Gradle & Android Studio
local.properties local.properties
local.sbt .gradle/
/proguard-sbt.txt .externalNativeBuild/
build/
captures/
release/
#Intellij IDEA
.idea/ .idea/
.d *.iml
gen/
mobile/src/main/libs core/src/overture/.deps
mobile/src/main/obj core/src/overture/bin
mobile/src/main/jni/overture core/src/overture/pkg
mobile/src/overture/.deps core/src/overture/src/github.com/Sirupsen
mobile/src/overture/bin core/src/overture/src/github.com/mattn
mobile/src/overture/pkg core/src/overture/src/github.com/miekg
core/src/overture/src/github.com/shawn1m
core/src/overture/src/github.com/tools
core/src/overture/src/golang.org
core/src/overture/src/gopkg.in
mobile/src/overture/src/github.com/Sirupsen # work in progress
mobile/src/overture/src/github.com/mattn tv/
mobile/src/overture/src/github.com/miekg things/
mobile/src/overture/src/github.com/shawn1m
mobile/src/overture/src/github.com/tools
mobile/src/overture/src/golang.org
mobile/src/overture/src/gopkg.in
[submodule "mobile/src/main/jni/shadowsocks-libev"] [submodule "core/src/main/jni/shadowsocks-libev"]
path = mobile/src/main/jni/shadowsocks-libev path = core/src/main/jni/shadowsocks-libev
url = https://github.com/shadowsocks/shadowsocks-libev url = https://github.com/shadowsocks/shadowsocks-libev
[submodule "mobile/src/main/jni/badvpn"] [submodule "core/src/main/jni/badvpn"]
path = mobile/src/main/jni/badvpn path = core/src/main/jni/badvpn
url = https://github.com/shadowsocks/badvpn.git url = https://github.com/shadowsocks/badvpn.git
branch = shadowsocks-android branch = shadowsocks-android
[submodule "mobile/src/main/jni/libancillary"] [submodule "core/src/main/jni/libancillary"]
path = mobile/src/main/jni/libancillary path = core/src/main/jni/libancillary
url = https://github.com/shadowsocks/libancillary.git url = https://github.com/shadowsocks/libancillary.git
branch = shadowsocks-android branch = shadowsocks-android
[submodule "mobile/src/main/jni/libevent"] [submodule "core/src/main/jni/libevent"]
path = mobile/src/main/jni/libevent path = core/src/main/jni/libevent
url = https://github.com/shadowsocks/libevent.git url = https://github.com/shadowsocks/libevent.git
branch = shadowsocks-android branch = shadowsocks-android
[submodule "mobile/src/main/jni/redsocks"] [submodule "core/src/main/jni/redsocks"]
path = mobile/src/main/jni/redsocks path = core/src/main/jni/redsocks
url = https://github.com/shadowsocks/redsocks.git url = https://github.com/shadowsocks/redsocks.git
branch = shadowsocks-android branch = shadowsocks-android
[submodule "mobile/src/main/jni/mbedtls"] [submodule "core/src/main/jni/mbedtls"]
path = mobile/src/main/jni/mbedtls path = core/src/main/jni/mbedtls
url = https://github.com/ARMmbed/mbedtls url = https://github.com/ARMmbed/mbedtls
[submodule "mobile/src/main/jni/pcre"] [submodule "core/src/main/jni/pcre"]
path = mobile/src/main/jni/pcre path = core/src/main/jni/pcre
url = https://android.googlesource.com/platform/external/pcre url = https://android.googlesource.com/platform/external/pcre
[submodule "mobile/src/main/jni/libsodium"] [submodule "core/src/main/jni/libsodium"]
path = mobile/src/main/jni/libsodium path = core/src/main/jni/libsodium
url = https://github.com/jedisct1/libsodium.git url = https://github.com/jedisct1/libsodium.git
branch = stable branch = stable
[submodule "mobile/src/main/jni/libev"] [submodule "core/src/main/jni/libev"]
path = mobile/src/main/jni/libev path = core/src/main/jni/libev
url = https://github.com/shadowsocks/libev.git url = https://github.com/shadowsocks/libev.git
[submodule "mobile/src/overture/go"] [submodule "core/src/overture/go"]
path = mobile/src/overture/go path = core/src/overture/go
url = https://github.com/shadowsocks/go.git url = https://github.com/shadowsocks/go.git
[submodule "mobile/src/overture/src/github.com/shadowsocks/overture"] [submodule "core/src/overture/src/github.com/shadowsocks/overture"]
path = mobile/src/overture/src/github.com/shadowsocks/overture path = core/src/overture/src/github.com/shadowsocks/overture
url = https://github.com/shadowsocks/overture.git url = https://github.com/shadowsocks/overture.git
# Scala source
".*\\.scala" -> frame open:"/*" line:"*" close:"*/"
-Xms1g
-Xmx3g
-Xss2m
-XX:+CMSClassUnloadingEnabled
-XX:+UseConcMarkSweepGC
...@@ -9,27 +9,18 @@ env: ...@@ -9,27 +9,18 @@ env:
- NDK_CCACHE=ccache - NDK_CCACHE=ccache
- GOROOT_BOOTSTRAP=$GOROOT - GOROOT_BOOTSTRAP=$GOROOT
- ANDROID_NDK_HOME=$HOME/.android/android-ndk-${NDK_VERSION} - ANDROID_NDK_HOME=$HOME/.android/android-ndk-${NDK_VERSION}
- SBTPATH=$HOME/.sbt
- PATH=${ANDROID_NDK_HOME}:${ANDROID_HOME}/tools:${ANDROID_HOME}/platform-tools:${PATH} - PATH=${ANDROID_NDK_HOME}:${ANDROID_HOME}/tools:${ANDROID_HOME}/platform-tools:${PATH}
scala:
- 2.11.11
before_cache:
- find $HOME/.sbt -name "*.lock" | xargs rm
- find $HOME/.ivy2 -name "*.lock" | xargs rm
cache: cache:
directories: directories:
- $HOME/.ivy2 - $HOME/.gradle
- $HOME/.sbt
android: android:
components: components:
- tools - tools
- android-27
- build-tools-26.0.2
- build-tools-27.0.0 - build-tools-27.0.0
- extra-android-m2repository
- extra-google-m2repository
install: install:
- > - >
...@@ -42,12 +33,6 @@ install: ...@@ -42,12 +33,6 @@ install:
popd; popd;
fi fi
- > - >
if [ ! -f "$SBTPATH/sbt" ]; then
pushd $SBTPATH;
wget -q https://raw.githubusercontent.com/paulp/sbt-extras/master/sbt;
chmod a+x sbt;
popd;
fi
script: script:
- $SBTPATH/sbt go-build android:package - ./gradlew assembleDebug
...@@ -3,18 +3,16 @@ ...@@ -3,18 +3,16 @@
[![Build Status](https://api.travis-ci.org/shadowsocks/shadowsocks-android.svg)](https://travis-ci.org/shadowsocks/shadowsocks-android) [![Build Status](https://api.travis-ci.org/shadowsocks/shadowsocks-android.svg)](https://travis-ci.org/shadowsocks/shadowsocks-android)
[![Releases](https://img.shields.io/github/downloads/shadowsocks/shadowsocks-android/total.svg)](https://github.com/shadowsocks/shadowsocks-android/releases) [![Releases](https://img.shields.io/github/downloads/shadowsocks/shadowsocks-android/total.svg)](https://github.com/shadowsocks/shadowsocks-android/releases)
A [shadowsocks](http://shadowsocks.org) client for Android, written in Scala. A [shadowsocks](http://shadowsocks.org) client for Android, written in Kotlin.
<a href="https://play.google.com/store/apps/details?id=com.github.shadowsocks"><img src="https://play.google.com/intl/en_us/badges/images/generic/en-play-badge.png" height="48"></a> <a href="https://play.google.com/store/apps/details?id=com.github.shadowsocks"><img src="https://play.google.com/intl/en_us/badges/images/generic/en-play-badge.png" height="48"></a>
### PREREQUISITES ### PREREQUISITES
* JDK 1.8 * JDK 1.8
* SBT 0.13.0+
* Go 1.4+ * Go 1.4+
* Android SDK * Android SDK
- Build Tools 26+ - Build Tools 27+
- Android Support Repository and Google Repository (see `build.sbt` for version)
- Android NDK r16+ - Android NDK r16+
### BUILD ### BUILD
...@@ -22,16 +20,8 @@ A [shadowsocks](http://shadowsocks.org) client for Android, written in Scala. ...@@ -22,16 +20,8 @@ A [shadowsocks](http://shadowsocks.org) client for Android, written in Scala.
* Set environment variable `ANDROID_HOME` to `/path/to/android-sdk` * Set environment variable `ANDROID_HOME` to `/path/to/android-sdk`
* (optional) Set environment variable `ANDROID_NDK_HOME` to `/path/to/android-ndk` (default: `$ANDROID_HOME/ndk-bundle`) * (optional) Set environment variable `ANDROID_NDK_HOME` to `/path/to/android-ndk` (default: `$ANDROID_HOME/ndk-bundle`)
* Set environment variable `GOROOT_BOOTSTRAP` to `/path/to/go` * Set environment variable `GOROOT_BOOTSTRAP` to `/path/to/go`
* Create your key following the instructions at https://developer.android.com/studio/publish/app-signing.html * Fetch submodules using `$ git submodule update --init --recursive`
* Create `mobile/local.properties` from `mobile/local.properties.example` with your own key information * Build it using Android Studio or gradle script
* Invoke the building like this
```bash
git submodule update --init --recursive
# Build the App
sbt clean go-build android:package-release
```
### TRANSLATE ### TRANSLATE
...@@ -54,7 +44,6 @@ Translators can go to [POEditor](https://poeditor.com/join/project/u5VHO9vhSf) t ...@@ -54,7 +44,6 @@ Translators can go to [POEditor](https://poeditor.com/join/project/u5VHO9vhSf) t
<li>overture: <a href="https://github.com/shawn1m/overture/blob/master/LICENSE">MIT</a></li> <li>overture: <a href="https://github.com/shawn1m/overture/blob/master/LICENSE">MIT</a></li>
<li>libev: <a href="https://github.com/shadowsocks/libev/blob/master/LICENSE">GPLv2</a></li> <li>libev: <a href="https://github.com/shadowsocks/libev/blob/master/LICENSE">GPLv2</a></li>
<li>libsodium: <a href="https://github.com/jedisct1/libsodium/blob/master/LICENSE">ISC</a></li> <li>libsodium: <a href="https://github.com/jedisct1/libsodium/blob/master/LICENSE">ISC</a></li>
<li>libudns: <a href="https://github.com/shadowsocks/libudns/blob/master/COPYING.LGPL">LGPL</a></li>
</ul> </ul>
### LICENSE ### LICENSE
......
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
ext {
kotlinVersion = '1.2.0'
minSdkVersion = 19
sdkVersion = 27
supportLibraryVersion = '27.0.2'
takisoftFixVersion = '27.0.2.0'
playServicesVersion = '11.6.2'
junitVersion = '4.12'
androidTestVersion = '1.0.1'
androidEspressoVersion = '3.0.1'
}
repositories {
google()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.0.1'
classpath 'com.google.gms:google-services:3.1.1'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"
}
}
allprojects {
repositories {
google()
jcenter()
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}
lazy val commonSettings = Seq(
scalaVersion := "2.11.12",
dexMaxHeap := "4g",
organization := "com.github.shadowsocks",
platformTarget := "android-27",
compileOrder := CompileOrder.JavaThenScala,
javacOptions ++= "-source" :: "1.7" :: "-target" :: "1.7" :: Nil,
scalacOptions ++= "-target:jvm-1.7" :: "-Xexperimental" :: Nil,
ndkArgs := "-j" :: java.lang.Runtime.getRuntime.availableProcessors.toString :: Nil,
ndkAbiFilter := Seq("armeabi-v7a", "arm64-v8a", "x86"),
proguardVersion := "5.3.3",
proguardCache := Seq(),
shrinkResources := true,
typedResources := false,
resConfigs := Seq("fa", "ja", "ko", "ru", "zh-rCN", "zh-rTW"),
resolvers += Resolver.jcenterRepo,
resolvers += Resolver.bintrayRepo("gericop", "maven"),
resolvers += "google" at "https://maven.google.com"
)
val supportLibsVersion = "27.0.2"
val takisoftFixVersion = "27.0.2.0"
lazy val root = Project(id = "shadowsocks-android", base = file("."))
.settings(commonSettings)
.aggregate(plugin, mobile)
install in Android := (install in (mobile, Android)).value
run in Android := (run in (mobile, Android)).evaluated
lazy val plugin = project
.settings(commonSettings)
.settings(
libraryDependencies ++=
"com.android.support" % "preference-v14" % supportLibsVersion ::
"com.takisoft.fix" % "preference-v7" % takisoftFixVersion ::
"com.takisoft.fix" % "preference-v7-simplemenu" % takisoftFixVersion ::
Nil
)
lazy val mobile = project
.settings(commonSettings)
.settings(
libraryDependencies ++=
"com.android.support" % "customtabs" % supportLibsVersion ::
"com.android.support" % "design" % supportLibsVersion ::
"com.android.support" % "gridlayout-v7" % supportLibsVersion ::
Nil
)
.dependsOn(plugin)
apply plugin: 'com.android.library'
android {
compileSdkVersion rootProject.sdkVersion
defaultConfig {
minSdkVersion rootProject.minSdkVersion
targetSdkVersion rootProject.sdkVersion
versionCode 1
versionName "1.0.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
externalNativeBuild {
ndkBuild {
abiFilters 'armeabi-v7a', 'arm64-v8a', 'x86'
arguments "-j${Runtime.runtime.availableProcessors()}"
}
}
}
buildTypes {
release {
minifyEnabled false
}
}
externalNativeBuild {
ndkBuild {
path 'src/main/jni/Android.mk'
}
}
}
task goBuild(type: Exec) {
executable "sh"
args "-c", "src/overture/make.bash " + minSdkVersion
}
task goClean(type: Exec) {
executable "sh"
args "-c", "src/overture/clean.bash"
}
tasks.whenTaskAdded { task ->
if (task.name == 'generateJsonModelDebug' ||
task.name == 'generateJsonModelRelease') {
task.dependsOn(goBuild)
}
}
dependencies {
api project(':plugin')
testImplementation "junit:junit:$junitVersion"
androidTestImplementation "com.android.support.test:runner:$androidTestVersion"
androidTestImplementation "com.android.support.test.espresso:espresso-core:$androidEspressoVersion"
}
package com.github.shadowsocks;
import android.content.Context;
import android.support.test.InstrumentationRegistry;
import android.support.test.runner.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
import static org.junit.Assert.*;
/**
* Instrumented test, which will execute on an Android device.
*
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
@RunWith(AndroidJUnit4.class)
public class ExampleInstrumentedTest {
@Test
public void useAppContext() throws Exception {
// Context of the app under test.
Context appContext = InstrumentationRegistry.getTargetContext();
assertEquals("com.github.shadowsockscorelibraries.test", appContext.getPackageName());
}
}
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.github.shadowsocks.core"/>
...@@ -476,17 +476,6 @@ LOCAL_SRC_FILES := $(addprefix pcre/, $(libpcre_src_files)) ...@@ -476,17 +476,6 @@ LOCAL_SRC_FILES := $(addprefix pcre/, $(libpcre_src_files))
include $(BUILD_STATIC_LIBRARY) include $(BUILD_STATIC_LIBRARY)
########################################################
## overture
########################################################
include $(CLEAR_VARS)
LOCAL_MODULE := overture
LOCAL_SRC_FILES := overture/$(TARGET_ARCH_ABI)/liboverture.so
include $(PREBUILT_SHARED_LIBRARY)
# Import cpufeatures # Import cpufeatures
$(call import-module,android/cpufeatures) $(call import-module,android/cpufeatures)
File moved
File moved
File moved
File moved
File moved
File moved
pcre @ 222bbf4b
File moved
File moved
#!/bin/bash
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
DEPS=$DIR/.deps
rm -rf $DEPS
rm -rf $DIR/go/bin
rm -rf $DIR/bin
echo "Successfully clean overture"
File moved
...@@ -8,7 +8,7 @@ function try () { ...@@ -8,7 +8,7 @@ function try () {
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
MIN_API=$1 MIN_API=$1
TARGET=$DIR/../main/jni/overture TARGET=$DIR/bin
DEPS=$DIR/.deps DEPS=$DIR/.deps
ANDROID_ARM_TOOLCHAIN=$DEPS/android-toolchain-${MIN_API}-arm ANDROID_ARM_TOOLCHAIN=$DEPS/android-toolchain-${MIN_API}-arm
...@@ -26,19 +26,19 @@ ANDROID_X86_STRIP=$ANDROID_X86_TOOLCHAIN/bin/i686-linux-android-strip ...@@ -26,19 +26,19 @@ ANDROID_X86_STRIP=$ANDROID_X86_TOOLCHAIN/bin/i686-linux-android-strip
try mkdir -p $DEPS $TARGET/armeabi-v7a $TARGET/x86 $TARGET/arm64-v8a try mkdir -p $DEPS $TARGET/armeabi-v7a $TARGET/x86 $TARGET/arm64-v8a
if [ ! -d "$ANDROID_ARM_TOOLCHAIN" ]; then if [ ! -f "$ANDROID_ARM_CC" ]; then
echo "Make standalone toolchain for ARM arch" echo "Make standalone toolchain for ARM arch"
$ANDROID_NDK_HOME/build/tools/make_standalone_toolchain.py --arch arm \ $ANDROID_NDK_HOME/build/tools/make_standalone_toolchain.py --arch arm \
--api $MIN_API --install-dir $ANDROID_ARM_TOOLCHAIN --api $MIN_API --install-dir $ANDROID_ARM_TOOLCHAIN
fi fi
if [ ! -d "$ANDROID_ARM64_TOOLCHAIN" ]; then if [ ! -f "$ANDROID_ARM64_CC" ]; then
echo "Make standalone toolchain for ARM64 arch" echo "Make standalone toolchain for ARM64 arch"
$ANDROID_NDK_HOME/build/tools/make_standalone_toolchain.py --arch arm64 \ $ANDROID_NDK_HOME/build/tools/make_standalone_toolchain.py --arch arm64 \
--api 21 --install-dir $ANDROID_ARM64_TOOLCHAIN --api 21 --install-dir $ANDROID_ARM64_TOOLCHAIN
fi fi
if [ ! -d "$ANDROID_X86_TOOLCHAIN" ]; then if [ ! -f "$ANDROID_X86_CC" ]; then
echo "Make standalone toolchain for X86 arch" echo "Make standalone toolchain for X86 arch"
$ANDROID_NDK_HOME/build/tools/make_standalone_toolchain.py --arch x86 \ $ANDROID_NDK_HOME/build/tools/make_standalone_toolchain.py --arch x86 \
--api $MIN_API --install-dir $ANDROID_X86_TOOLCHAIN --api $MIN_API --install-dir $ANDROID_X86_TOOLCHAIN
...@@ -63,19 +63,25 @@ pushd $GOPATH/src/github.com/shadowsocks/overture/main ...@@ -63,19 +63,25 @@ pushd $GOPATH/src/github.com/shadowsocks/overture/main
godep restore godep restore
echo "Cross compile overture for arm" echo "Cross compile overture for arm"
try env CGO_ENABLED=1 CC=$ANDROID_ARM_CC GOOS=android GOARCH=arm GOARM=7 go build -ldflags="-s -w" if [ ! -f "$TARGET/armeabi-v7a/liboverture.so" ]; then
try $ANDROID_ARM_STRIP main try env CGO_ENABLED=1 CC=$ANDROID_ARM_CC GOOS=android GOARCH=arm GOARM=7 go build -ldflags="-s -w"
try mv main $TARGET/armeabi-v7a/liboverture.so try $ANDROID_ARM_STRIP main
try mv main $TARGET/armeabi-v7a/liboverture.so
fi
echo "Cross compile overture for arm64" echo "Cross compile overture for arm64"
try env CGO_ENABLED=1 CC=$ANDROID_ARM64_CC GOOS=android GOARCH=arm64 go build -ldflags="-s -w" if [ ! -f "$TARGET/arm64-v8a/liboverture.so" ]; then
try $ANDROID_ARM64_STRIP main try env CGO_ENABLED=1 CC=$ANDROID_ARM64_CC GOOS=android GOARCH=arm64 go build -ldflags="-s -w"
try mv main $TARGET/arm64-v8a/liboverture.so try $ANDROID_ARM64_STRIP main
try mv main $TARGET/arm64-v8a/liboverture.so
fi
echo "Cross compile overture for x86" echo "Cross compile overture for x86"
try env CGO_ENABLED=1 CC=$ANDROID_X86_CC GOOS=android GOARCH=386 go build -ldflags="-s -w" if [ ! -f "$TARGET/x86/liboverture.so" ]; then
try $ANDROID_X86_STRIP main try env CGO_ENABLED=1 CC=$ANDROID_X86_CC GOOS=android GOARCH=386 go build -ldflags="-s -w"
try mv main $TARGET/x86/liboverture.so try $ANDROID_X86_STRIP main
try mv main $TARGET/x86/liboverture.so
fi
popd popd
......
File moved
package com.github.shadowsocks;
import org.junit.Test;
import static org.junit.Assert.*;
/**
* Example local unit test, which will execute on the development machine (host).
*
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
public class ExampleUnitTest {
@Test
public void addition_isCorrect() throws Exception {
assertEquals(4, 2 + 2);
}
}
\ No newline at end of file
# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx1536m
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
#Fri Dec 01 17:03:17 PST 2017
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-4.4-all.zip
#!/usr/bin/env bash
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS=""
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn ( ) {
echo "$*"
}
die ( ) {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
esac
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin, switch paths to Windows format before running java
if $cygwin ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=$((i+1))
done
case $i in
(0) set -- ;;
(1) set -- "$args0" ;;
(2) set -- "$args0" "$args1" ;;
(3) set -- "$args0" "$args1" "$args2" ;;
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
function splitJvmOpts() {
JVM_OPTS=("$@")
}
eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS=
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto init
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto init
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:init
@rem Get command-line arguments, handling Windowz variants
if not "%OS%" == "Windows_NT" goto win9xME_args
if "%@eval[2+2]" == "4" goto 4NT_args
:win9xME_args
@rem Slurp the command line arguments.
set CMD_LINE_ARGS=
set _SKIP=2
:win9xME_args_slurp
if "x%~1" == "x" goto execute
set CMD_LINE_ARGS=%*
goto execute
:4NT_args
@rem Get arguments from the 4NT Shell from JP Software
set CMD_LINE_ARGS=%$
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
import java.util.regex.Matcher
import java.util.regex.Pattern
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
def getCurrentFlavor() {
Matcher matcher = Pattern.compile("(assemble|generate)(\\w+)(Release|Debug)")
.matcher(getGradle().getStartParameter().getTaskRequests().toString())
if (matcher.find()) return matcher.group(1).toLowerCase() else {
println "NO MATCH FOUND"
return ""
}
}
android {
compileSdkVersion rootProject.sdkVersion
defaultConfig {
applicationId "com.github.shadowsocks"
minSdkVersion rootProject.minSdkVersion
targetSdkVersion rootProject.sdkVersion
versionCode 200
versionName "4.4.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
vectorDrawables.useSupportLibrary true
resConfigs "fa", "ja", "ko", "ru", "zh-rCN", "zh-rTW"
}
buildTypes {
debug {
pseudoLocalesEnabled true
}
release {
shrinkResources true
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
lintOptions {
abortOnError false
}
sourceSets.main.jniLibs.srcDirs +=
new File(project(':core').buildDir, "intermediates/bundles/${getCurrentFlavor()}/jni")
sourceSets.main.jniLibs.srcDirs += new File(project(':core').projectDir, "src/overture/bin")
}
dependencies {
implementation project(':core')
implementation "com.android.support:customtabs:$supportLibraryVersion"
implementation "com.android.support:design:$supportLibraryVersion"
implementation "com.android.support:gridlayout-v7:$supportLibraryVersion"
implementation 'com.futuremind.recyclerfastscroll:fastscroll:0.2.5'
implementation 'com.evernote:android-job:1.2.1'
implementation 'com.github.jorgecastilloprz:fabprogresscircle:1.01'
implementation "com.google.android.gms:play-services-ads:$playServicesVersion"
implementation "com.google.android.gms:play-services-analytics:$playServicesVersion"
implementation "com.google.android.gms:play-services-gcm:$playServicesVersion"
implementation "com.google.firebase:firebase-config:$playServicesVersion"
implementation 'com.j256.ormlite:ormlite-android:5.0'
implementation 'com.mikepenz:crossfader:1.5.1'
implementation 'com.mikepenz:fastadapter:3.0.4'
implementation 'com.mikepenz:iconics-core:3.0.0'
implementation 'com.mikepenz:materialdrawer:6.0.2'
implementation 'com.mikepenz:materialize:1.1.2'
implementation 'com.squareup.okhttp3:okhttp:3.9.1'
implementation "com.takisoft.fix:preference-v7-simplemenu:$takisoftFixVersion"
implementation 'com.twofortyfouram:android-plugin-api-for-locale:1.0.2'
implementation 'dnsjava:dnsjava:2.1.8'
implementation 'eu.chainfire:libsuperuser:1.0.0.201704021214'
implementation 'me.dm7.barcodescanner:zxing:1.9.8'
implementation 'net.glxn.qrgen:android:2.0'
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlinVersion"
testImplementation "junit:junit:$junitVersion"
androidTestImplementation "com.android.support.test:runner:$androidTestVersion"
androidTestImplementation "com.android.support.test.espresso:espresso-core:$androidEspressoVersion"
}
repositories {
mavenCentral()
}
apply plugin: 'com.google.gms.google-services'
enablePlugins(AndroidApp)
enablePlugins(AndroidGms)
android.useSupportVectors
name := "shadowsocks"
version := "4.3.2"
versionCode := Some(198)
proguardOptions ++=
"-dontwarn com.google.android.gms.internal.**" ::
"-dontwarn com.j256.ormlite.**" ::
"-dontwarn okhttp3.**" ::
"-dontwarn okio.**" ::
"-dontwarn org.xbill.**" ::
"-keep class com.github.shadowsocks.JniHelper { *; }" ::
"-dontwarn com.evernote.android.job.gcm.**" ::
"-dontwarn com.evernote.android.job.util.GcmAvailableHelper" ::
"-keep public class com.evernote.android.job.v21.PlatformJobService" ::
"-keep public class com.evernote.android.job.v14.PlatformAlarmService" ::
"-keep public class com.evernote.android.job.v14.PlatformAlarmReceiver" ::
"-keep public class com.evernote.android.job.JobBootReceiver" ::
"-keep public class com.evernote.android.job.JobRescheduleService" ::
Nil
val playServicesVersion = "11.6.2"
resolvers += Resolver.jcenterRepo
libraryDependencies ++=
"com.futuremind.recyclerfastscroll" % "fastscroll" % "0.2.5" ::
"com.evernote" % "android-job" % "1.2.1" ::
"com.github.jorgecastilloprz" % "fabprogresscircle" % "1.01" ::
"com.google.android.gms" % "play-services-ads" % playServicesVersion ::
"com.google.android.gms" % "play-services-analytics" % playServicesVersion ::
"com.google.android.gms" % "play-services-gcm" % playServicesVersion ::
"com.google.firebase" % "firebase-config" % playServicesVersion ::
"com.j256.ormlite" % "ormlite-android" % "5.0" ::
"com.mikepenz" % "crossfader" % "1.5.1" ::
"com.mikepenz" % "fastadapter" % "3.0.4" ::
"com.mikepenz" % "iconics-core" % "3.0.0" ::
"com.mikepenz" % "materialdrawer" % "6.0.2" ::
"com.mikepenz" % "materialize" % "1.1.2" ::
"com.squareup.okhttp3" % "okhttp" % "3.9.1" ::
"com.twofortyfouram" % "android-plugin-api-for-locale" % "1.0.2" ::
"dnsjava" % "dnsjava" % "2.1.8" ::
"eu.chainfire" % "libsuperuser" % "1.0.0.201704021214" ::
"me.dm7.barcodescanner" % "zxing" % "1.9.8" ::
"net.glxn.qrgen" % "android" % "2.0" ::
Nil
packagingOptions := PackagingOptions(excludes =
"META-INF/maven/com.squareup.okio/okio/pom.properties" ::
"META-INF/maven/com.squareup.okio/okio/pom.xml" ::
"META-INF/maven/com.squareup.okhttp3/okhttp/pom.properties" ::
"META-INF/maven/com.squareup.okhttp3/okhttp/pom.xml" ::
Nil)
lazy val goClean: TaskKey[Unit] = TaskKey[Unit]("go-clean", "Clean go build dependencies")
goClean := {
IO.delete(baseDirectory(base => base / "src/overture/.deps").value)
IO.delete(baseDirectory(base => base / "src/overture/bin").value)
IO.delete(baseDirectory(base => base / "src/overture/go/bin").value)
IO.delete(baseDirectory(base => base / "src/main/jni/overture").value)
}
lazy val goBuild: TaskKey[Unit] = TaskKey[Unit]("go-build", "Build go and overture")
goBuild := {
Process(Seq("mobile/src/overture/make.bash", minSdkVersion.value)) ! streams.value.log match {
case 0 => // Success!
case n => sys.error(s"Native build script exit code: $n")
}
}
<issue id="NewApi">
<ignore path="src/main/res/drawable"/>
</issue>
key.alias: your_key_alias
key.store: /path/to/your/key/store
key.store.password: your_key_password
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# 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 *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
-keepattributes SourceFile,LineNumberTable
-dontobfuscate
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
-dontwarn com.j256.ormlite.**
-dontwarn com.google.android.gms.internal.**
-dontwarn okhttp3.**
-dontwarn okio.**
-dontwarn org.xbill.**
-dontwarn com.evernote.android.job.gcm.**
-dontwarn com.evernote.android.job.util.GcmAvailableHelper
package com.github.shadowsocks;
import android.content.Context;
import android.support.test.InstrumentationRegistry;
import android.support.test.runner.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
import static org.junit.Assert.*;
/**
* Instrumented test, which will execute on an Android device.
*
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
@RunWith(AndroidJUnit4.class)
public class ExampleInstrumentedTest {
@Test
public void useAppContext() throws Exception {
// Context of the app under test.
Context appContext = InstrumentationRegistry.getTargetContext();
assertEquals("com.github.shadowsocks", appContext.getPackageName());
}
}
<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.github.shadowsocks"> xmlns:tools="http://schemas.android.com/tools"
package="com.github.shadowsocks">
<uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/> <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
...@@ -15,17 +16,14 @@ ...@@ -15,17 +16,14 @@
<uses-feature android:name="android.hardware.camera" <uses-feature android:name="android.hardware.camera"
android:required="false"/> android:required="false"/>
<uses-sdk
android:minSdkVersion="19"
android:targetSdkVersion="27"/>
<application <application
android:name=".ShadowsocksApplication" android:name=".App"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:backupAgent=".ShadowsocksBackupAgent" android:backupAgent=".ConfigBackupHelper"
android:label="@string/app_name" android:label="@string/app_name"
android:networkSecurityConfig="@xml/network_security_config" android:supportsRtl="true"
android:banner="@drawable/ic_start_connected"> android:networkSecurityConfig="@xml/network_security_config"
android:banner="@drawable/ic_start_connected">
<meta-data android:name="android.webkit.WebView.EnableSafeBrowsing" <meta-data android:name="android.webkit.WebView.EnableSafeBrowsing"
android:value="true" /> android:value="true" />
...@@ -33,11 +31,14 @@ ...@@ -33,11 +31,14 @@
android:value="@integer/google_play_services_version"/> android:value="@integer/google_play_services_version"/>
<meta-data android:name="com.google.android.backup.api_key" <meta-data android:name="com.google.android.backup.api_key"
android:value="AEdPqrEAAAAI_zVxZthz2HDuz9toTvkYvL0L5GA-OjeUIfBeXg"/> android:value="AEdPqrEAAAAI_zVxZthz2HDuz9toTvkYvL0L5GA-OjeUIfBeXg"/>
<meta-data
android:name="preloaded_fonts"
android:resource="@array/preloaded_fonts"/>
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:label="@string/app_name" android:label="@string/app_name"
android:launchMode="singleTask"> android:launchMode="singleTask">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN"/> <action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/> <category android:name="android.intent.category.LAUNCHER"/>
...@@ -62,32 +63,32 @@ ...@@ -62,32 +63,32 @@
</activity> </activity>
<activity <activity
android:name=".ProfileConfigActivity" android:name=".ProfileConfigActivity"
android:excludeFromRecents="true" android:excludeFromRecents="true"
android:label="@string/profile_config" android:label="@string/profile_config"
android:launchMode="singleTask"/> android:launchMode="singleTask"/>
<activity <activity
android:name=".ShadowsocksRunnerActivity" android:name=".VpnRequestActivity"
android:theme="@android:style/Theme.Translucent.NoTitleBar" android:theme="@android:style/Theme.Translucent.NoTitleBar"
android:excludeFromRecents="true" android:excludeFromRecents="true"
android:taskAffinity="" android:taskAffinity=""
android:launchMode="singleTask"/> android:launchMode="singleTask"/>
<activity <activity
android:name=".AppManager" android:name=".AppManager"
android:label="@string/proxied_apps" android:label="@string/proxied_apps"
android:parentActivityName=".ProfileConfigActivity" android:parentActivityName=".ProfileConfigActivity"
android:excludeFromRecents="true" android:excludeFromRecents="true"
android:launchMode="singleTask"/> android:launchMode="singleTask"/>
<activity <activity
android:name=".ScannerActivity" android:name=".ScannerActivity"
android:label="@string/add_profile_methods_scan_qr_code" android:label="@string/add_profile_methods_scan_qr_code"
android:parentActivityName=".MainActivity" android:parentActivityName=".MainActivity"
android:excludeFromRecents="true"/> android:excludeFromRecents="true"/>
<activity android:name=".TaskerActivity" <activity android:name=".tasker.ConfigActivity"
android:icon="@mipmap/ic_launcher"> android:icon="@mipmap/ic_launcher">
<intent-filter> <intent-filter>
<action android:name="com.twofortyfouram.locale.intent.action.EDIT_SETTING" /> <action android:name="com.twofortyfouram.locale.intent.action.EDIT_SETTING" />
...@@ -95,24 +96,24 @@ ...@@ -95,24 +96,24 @@
</activity> </activity>
<activity <activity
android:name=".QuickToggleShortcut" android:name=".QuickToggleShortcut"
android:label="@string/quick_toggle" android:label="@string/quick_toggle"
android:theme="@android:style/Theme.Translucent.NoTitleBar" android:theme="@android:style/Theme.Translucent.NoTitleBar"
android:excludeFromRecents="true" android:excludeFromRecents="true"
android:taskAffinity="" android:taskAffinity=""
android:process=":bg" android:process=":bg"
android:launchMode="singleTask"> android:launchMode="singleTask">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.CREATE_SHORTCUT" /> <action android:name="android.intent.action.CREATE_SHORTCUT" />
</intent-filter> </intent-filter>
</activity> </activity>
<service <service
android:name=".bg.VpnService" android:name=".bg.VpnService"
android:process=":bg" android:process=":bg"
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:exported="false"> android:exported="false">
<intent-filter> <intent-filter>
<action android:name="android.net.VpnService"/> <action android:name="android.net.VpnService"/>
</intent-filter> </intent-filter>
...@@ -130,7 +131,7 @@ ...@@ -130,7 +131,7 @@
android:exported="false"> android:exported="false">
</service> </service>
<service android:name=".bg.ServiceTileService" android:label="@string/quick_toggle" <service android:name=".bg.TileService" android:label="@string/quick_toggle"
android:process=":bg" android:process=":bg"
android:icon="@drawable/ic_start_connected" android:icon="@drawable/ic_start_connected"
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE"> android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
...@@ -145,7 +146,7 @@ ...@@ -145,7 +146,7 @@
</intent-filter> </intent-filter>
</receiver> </receiver>
<receiver android:name=".TaskerReceiver" android:process=":bg" tools:ignore="ExportedReceiver"> <receiver android:name=".tasker.ActionListener" android:process=":bg" 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>
...@@ -165,5 +166,4 @@ ...@@ -165,5 +166,4 @@
<receiver android:name="com.evernote.android.job.JobBootReceiver" <receiver android:name="com.evernote.android.job.JobBootReceiver"
android:process=":bg"/> android:process=":bg"/>
</application> </application>
</manifest> </manifest>
...@@ -273,7 +273,7 @@ img { ...@@ -273,7 +273,7 @@ img {
</style> </style>
</head> </head>
<body> <body>
<p>A <a href="http://shadowsocks.org">Shadowsocks</a> client for Android, written in Scala.</p> <p>A <a href="http://shadowsocks.org">Shadowsocks</a> client for Android, written in Kotlin.</p>
<p>Copyright (C) 2017 by Max Lv <a href="&#x6d;&#97;&#x69;&#108;&#116;&#111;&#x3a;&#x6d;&#97;x&#46;&#99;.&#108;&#118;&#x40;&#103;&#109;&#97;&#x69;&#108;&#46;&#x63;&#x6f;&#109;">&#x6d;&#97;&#x78;.&#x63;&#x2e;&#108;v&#64;&#x67;&#109;&#x61;&#x69;&#108;&#x2e;&#99;&#111;&#109;</a></p> <p>Copyright (C) 2017 by Max Lv <a href="&#x6d;&#97;&#x69;&#108;&#116;&#111;&#x3a;&#x6d;&#97;x&#46;&#99;.&#108;&#118;&#x40;&#103;&#109;&#97;&#x69;&#108;&#46;&#x63;&#x6f;&#109;">&#x6d;&#97;&#x78;.&#x63;&#x2e;&#108;v&#64;&#x67;&#109;&#x61;&#x69;&#108;&#x2e;&#99;&#111;&#109;</a></p>
<p>Copyright (C) 2017 by Mygod Studio <a href="mailto:contact-shadowsocks-android@mygod.be">contact-shadowsocks-android@mygod.be</a></p> <p>Copyright (C) 2017 by Mygod Studio <a href="mailto:contact-shadowsocks-android@mygod.be">contact-shadowsocks-android@mygod.be</a></p>
<p>This program is free software: you can redistribute it and/or modify <p>This program is free software: you can redistribute it and/or modify
...@@ -299,7 +299,6 @@ along with this program. If not, see <a href="http://www.gnu.org/licenses/">http ...@@ -299,7 +299,6 @@ along with this program. If not, see <a href="http://www.gnu.org/licenses/">http
<li>overture: <a href="https://github.com/shawn1m/overture/blob/master/LICENSE">MIT</a></li> <li>overture: <a href="https://github.com/shawn1m/overture/blob/master/LICENSE">MIT</a></li>
<li>libev: <a href="https://github.com/shadowsocks/libev/blob/master/LICENSE">GPLv2</a></li> <li>libev: <a href="https://github.com/shadowsocks/libev/blob/master/LICENSE">GPLv2</a></li>
<li>libsodium: <a href="https://github.com/jedisct1/libsodium/blob/master/LICENSE">ISC</a></li> <li>libsodium: <a href="https://github.com/jedisct1/libsodium/blob/master/LICENSE">ISC</a></li>
<li>libudns: <a href="https://github.com/shadowsocks/libudns/blob/master/COPYING.LGPL">LGPL</a></li>
</ul> </ul>
</body> </body>
......
/*******************************************************************************
* *
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
* *
*******************************************************************************/
package com.github.shadowsocks
import android.annotation.TargetApi
import android.os.Build
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.webkit.WebResourceRequest
import android.webkit.WebView
import android.webkit.WebViewClient
class AboutFragment : ToolbarFragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? =
inflater.inflate(R.layout.layout_about, container, false)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
toolbar.title = getString(R.string.about_title, BuildConfig.VERSION_NAME)
val web = view.findViewById<WebView>(R.id.web_view)
web.loadUrl("file:///android_asset/pages/about.html")
web.webViewClient = object : WebViewClient() {
@Suppress("OverridingDeprecatedMember")
override fun shouldOverrideUrlLoading(view: WebView, url: String): Boolean {
(activity as MainActivity).launchUrl(url)
return true
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
override fun shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest): Boolean {
(activity as MainActivity).launchUrl(request.url)
return true
}
}
}
}
/*******************************************************************************
* *
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
* *
*******************************************************************************/
package com.github.shadowsocks
import android.app.Application
import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.BroadcastReceiver
import android.content.Intent
import android.content.IntentFilter
import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import android.content.res.Configuration
import android.os.Build
import android.os.Handler
import android.os.LocaleList
import android.os.Looper
import android.support.annotation.RequiresApi
import android.support.v7.app.AppCompatDelegate
import android.util.Log
import com.evernote.android.job.JobManager
import com.github.shadowsocks.acl.AclSyncJob
import com.github.shadowsocks.bg.BaseService
import com.github.shadowsocks.database.Profile
import com.github.shadowsocks.database.ProfileManager
import com.github.shadowsocks.preference.BottomSheetPreferenceDialogFragment
import com.github.shadowsocks.preference.DataStore
import com.github.shadowsocks.preference.IconListPreference
import com.github.shadowsocks.utils.Action
import com.github.shadowsocks.utils.Key
import com.github.shadowsocks.utils.TcpFastOpen
import com.github.shadowsocks.utils.broadcastReceiver
import com.google.android.gms.analytics.GoogleAnalytics
import com.google.android.gms.analytics.HitBuilders
import com.google.android.gms.analytics.StandardExceptionParser
import com.google.android.gms.analytics.Tracker
import com.google.firebase.FirebaseApp
import com.google.firebase.remoteconfig.FirebaseRemoteConfig
import com.j256.ormlite.logger.LocalLog
import com.takisoft.fix.support.v7.preference.PreferenceFragmentCompat
import java.io.File
import java.io.IOException
import java.util.*
class App : Application() {
companion object {
lateinit var app: App
private const val TAG = "ShadowsocksApplication"
// The ones in Locale doesn't have script included
private val SIMPLIFIED_CHINESE by lazy {
if (Build.VERSION.SDK_INT >= 21) Locale.forLanguageTag("zh-Hans-CN") else Locale.SIMPLIFIED_CHINESE
}
private val TRADITIONAL_CHINESE by lazy {
if (Build.VERSION.SDK_INT >= 21) Locale.forLanguageTag("zh-Hant-TW") else Locale.TRADITIONAL_CHINESE
}
}
val handler by lazy { Handler(Looper.getMainLooper()) }
val remoteConfig: FirebaseRemoteConfig by lazy { FirebaseRemoteConfig.getInstance() }
private val tracker: Tracker by lazy { GoogleAnalytics.getInstance(this).newTracker(R.xml.tracker) }
val info: PackageInfo by lazy { packageManager.getPackageInfo(packageName, PackageManager.GET_SIGNATURES) }
fun startService() {
val intent = Intent(this, BaseService.serviceClass.java)
if (Build.VERSION.SDK_INT >= 26) startForegroundService(intent) else startService(intent)
}
fun reloadService() = sendBroadcast(Intent(Action.RELOAD))
fun stopService() = sendBroadcast(Intent(Action.CLOSE))
val currentProfile: Profile? get() = ProfileManager.getProfile(DataStore.profileId)
fun switchProfile(id: Int): Profile {
val result = ProfileManager.getProfile(id) ?: ProfileManager.createProfile()
DataStore.profileId = result.id
return result
}
// send event
fun track(category: String, action: String) = tracker.send(HitBuilders.EventBuilder()
.setCategory(category)
.setAction(action)
.setLabel(BuildConfig.VERSION_NAME)
.build())
fun track(t: Throwable) = track(Thread.currentThread(), t)
fun track(thread: Thread, t: Throwable) {
tracker.send(HitBuilders.ExceptionBuilder()
.setDescription(StandardExceptionParser(this, null).getDescription(thread.name, t))
.setFatal(false)
.build())
t.printStackTrace()
}
private fun checkChineseLocale(config: Configuration) {
fun check(locale: Locale): Locale? {
if (locale.language != "zh") return null
when (locale.country) { "CN", "TW" -> return null }
if (Build.VERSION.SDK_INT >= 21) when (locale.script) {
"Hans" -> return SIMPLIFIED_CHINESE
"Hant" -> return TRADITIONAL_CHINESE
else -> Log.w(TAG, "Unknown zh locale script: ${locale.script}. Falling back to trying countries...")
}
when (locale.country) {
"SG" -> return SIMPLIFIED_CHINESE
"HK", "MO" -> return TRADITIONAL_CHINESE
}
Log.w(TAG, "Unknown zh locale: %s. Falling back to zh-Hans-CN..."
.format(Locale.ENGLISH, if (Build.VERSION.SDK_INT >= 21) locale.toLanguageTag() else locale))
return SIMPLIFIED_CHINESE
}
if (Build.VERSION.SDK_INT >= 24) @RequiresApi(24) {
val localeList = config.locales
var changed = false
val newList = Array<Locale>(localeList.size(), { i ->
val locale = localeList[i]
val newLocale = check(locale)
if (newLocale == null) locale else {
changed = true
newLocale
}
})
if (changed) {
val newConfig = Configuration(config)
newConfig.locales = LocaleList(*(newList.distinct().toTypedArray()))
val res = resources
res.updateConfiguration(newConfig, res.displayMetrics)
}
} else {
@Suppress("DEPRECATION")
val newLocale = check(config.locale)
if (newLocale != null) {
val newConfig = Configuration(config)
@Suppress("DEPRECATION")
newConfig.locale = newLocale
val res = resources
res.updateConfiguration(newConfig, res.displayMetrics)
}
}
}
override fun onCreate() {
super.onCreate()
app = this
if (!BuildConfig.DEBUG) System.setProperty(LocalLog.LOCAL_LOG_LEVEL_PROPERTY, "ERROR")
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true)
checkChineseLocale(resources.configuration)
FirebaseApp.initializeApp(this)
remoteConfig.setDefaults(R.xml.default_configs)
remoteConfig.fetch().addOnCompleteListener {
if (it.isSuccessful) remoteConfig.activateFetched() else Log.e(TAG, "Failed to fetch config")
}
JobManager.create(this).addJobCreator(AclSyncJob)
PreferenceFragmentCompat.registerPreferenceFragment(IconListPreference::class.java,
BottomSheetPreferenceDialogFragment::class.java)
TcpFastOpen.enabled(DataStore.getBoolean(Key.tfo, TcpFastOpen.sendEnabled))
if (DataStore.getLong(Key.assetUpdateTime, -1) != info.lastUpdateTime) {
val assetManager = assets
for (dir in arrayOf("acl", "overture"))
try {
for (file in assetManager.list(dir)) assetManager.open(dir + '/' + file).use { input ->
File(filesDir, file).outputStream().use { output -> input.copyTo(output) }
}
} catch (e: IOException) {
Log.e(TAG, e.message)
app.track(e)
}
DataStore.putLong(Key.assetUpdateTime, info.lastUpdateTime)
}
if (Build.VERSION.SDK_INT >= 26) @RequiresApi(26) {
val nm = getSystemService(NotificationManager::class.java)
nm.createNotificationChannels(listOf(
NotificationChannel("service-vpn", getText(R.string.service_vpn),
NotificationManager.IMPORTANCE_LOW),
NotificationChannel("service-proxy", getText(R.string.service_proxy),
NotificationManager.IMPORTANCE_LOW),
NotificationChannel("service-transproxy", getText(R.string.service_transproxy),
NotificationManager.IMPORTANCE_LOW)))
nm.deleteNotificationChannel("service-nat") // NAT mode is gone for good
}
}
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
checkChineseLocale(newConfig)
}
fun listenForPackageChanges(callback: () -> Unit): BroadcastReceiver {
val filter = IntentFilter(Intent.ACTION_PACKAGE_ADDED)
filter.addAction(Intent.ACTION_PACKAGE_REMOVED)
filter.addDataScheme("package")
val result = broadcastReceiver { _, intent ->
if (intent.action != Intent.ACTION_PACKAGE_REMOVED ||
!intent.getBooleanExtra(Intent.EXTRA_REPLACING, false)) callback()
}
app.registerReceiver(result, filter)
return result
}
}
/*******************************************************************************
* *
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
* *
*******************************************************************************/
package com.github.shadowsocks
import android.Manifest
import android.animation.Animator
import android.animation.AnimatorListenerAdapter
import android.annotation.SuppressLint
import android.content.BroadcastReceiver
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.content.pm.PackageManager
import android.graphics.drawable.Drawable
import android.os.Bundle
import android.os.Handler
import android.support.v4.app.TaskStackBuilder
import android.support.v7.app.AppCompatActivity
import android.support.v7.widget.DefaultItemAnimator
import android.support.v7.widget.LinearLayoutManager
import android.support.v7.widget.RecyclerView
import android.support.v7.widget.Toolbar
import android.view.*
import android.widget.ImageView
import android.widget.Switch
import android.widget.Toast
import com.futuremind.recyclerviewfastscroll.FastScroller
import com.futuremind.recyclerviewfastscroll.SectionTitleProvider
import com.github.shadowsocks.App.Companion.app
import com.github.shadowsocks.database.ProfileManager
import com.github.shadowsocks.preference.DataStore
import com.github.shadowsocks.utils.Key
import com.github.shadowsocks.utils.thread
import java.util.concurrent.atomic.AtomicBoolean
class AppManager : AppCompatActivity(), Toolbar.OnMenuItemClickListener {
companion object {
data class ProxiedApp(val name: String, val packageName: String, val icon: Drawable)
@SuppressLint("StaticFieldLeak")
private var instance: AppManager? = null
private var receiver: BroadcastReceiver? = null
private var cachedApps: Array<ProxiedApp>? = null
private fun getApps(pm: PackageManager): Array<ProxiedApp> {
if (receiver == null) receiver = app.listenForPackageChanges {
synchronized(AppManager) { cachedApps = null }
AppManager.instance?.reloadApps()
}
return synchronized(AppManager) {
val cachedApps = cachedApps ?: pm.getInstalledPackages(PackageManager.GET_PERMISSIONS)
.filter { it.requestedPermissions?.contains(Manifest.permission.INTERNET) ?: false }
.map { ProxiedApp(pm.getApplicationLabel(it.applicationInfo).toString(), it.packageName,
it.applicationInfo.loadIcon(pm)) }
.toTypedArray()
this.cachedApps = cachedApps
cachedApps
}
}
}
private inner class AppViewHolder(view: View) : RecyclerView.ViewHolder(view), View.OnClickListener {
private val icon = view.findViewById<ImageView>(R.id.itemicon)
private val check = view.findViewById<Switch>(R.id.itemcheck)
private lateinit var item: ProxiedApp
private val proxied get() = proxiedApps.contains(item.packageName)
init {
view.setOnClickListener(this)
}
fun bind(app: ProxiedApp) {
this.item = app
icon.setImageDrawable(app.icon)
check.text = app.name
check.isChecked = proxied
}
override fun onClick(v: View?) {
if (proxied) {
proxiedApps.remove(item.packageName)
check.isChecked = false
} else {
proxiedApps.add(item.packageName)
check.isChecked = true
}
if (!appsLoading.get()) {
DataStore.individual = proxiedApps.joinToString("\n")
DataStore.dirty = true
}
}
}
private inner class AppsAdapter : RecyclerView.Adapter<AppViewHolder>(), SectionTitleProvider {
private var apps = listOf<ProxiedApp>()
fun reload() {
apps = getApps(packageManager).sortedWith(Comparator { a, b ->
when (Pair(proxiedApps.contains(a.packageName), proxiedApps.contains(b.packageName))) {
Pair(true, false) -> -1
Pair(false, true) -> 1
else -> a.name.compareTo(b.name, true)
}
})
}
override fun onBindViewHolder(holder: AppViewHolder, position: Int) = holder.bind(apps[position])
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AppViewHolder =
AppViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.layout_apps_item, parent, false))
override fun getItemCount(): Int = apps.size
override fun getSectionTitle(position: Int): String = apps[position].name.substring(0, 1)
}
private lateinit var proxiedApps: HashSet<String>
private lateinit var toolbar: Toolbar
private lateinit var bypassSwitch: Switch
private lateinit var appListView: RecyclerView
private lateinit var fastScroller: FastScroller
private lateinit var loadingView: View
private val appsLoading = AtomicBoolean()
private val handler = Handler()
private val clipboard by lazy { getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager }
private fun initProxiedApps(str: String = DataStore.individual) {
proxiedApps = str.split('\n').toHashSet()
}
private fun reloadApps() {
if (!appsLoading.compareAndSet(true, false)) loadAppsAsync()
}
private fun loadAppsAsync() {
if (!appsLoading.compareAndSet(false, true)) return
appListView.visibility = View.GONE
fastScroller.visibility = View.GONE
loadingView.visibility = View.VISIBLE
thread {
val adapter = appListView.adapter as AppsAdapter
do {
appsLoading.set(true)
adapter.reload()
} while (!appsLoading.compareAndSet(true, false))
handler.post {
adapter.notifyDataSetChanged()
val shortAnimTime = resources.getInteger(android.R.integer.config_shortAnimTime)
appListView.alpha = 0F
appListView.visibility = View.VISIBLE
appListView.animate().alpha(1F).duration = shortAnimTime.toLong()
fastScroller.alpha = 0F
fastScroller.visibility = View.VISIBLE
fastScroller.animate().alpha(1F).duration = shortAnimTime.toLong()
loadingView.animate().alpha(0F).setListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
loadingView.visibility = View.GONE
}
}).duration = shortAnimTime.toLong()
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.layout_apps)
toolbar = findViewById(R.id.toolbar)
toolbar.setTitle(R.string.proxied_apps)
toolbar.setNavigationIcon(R.drawable.abc_ic_ab_back_material)
toolbar.setNavigationOnClickListener {
val intent = parentActivityIntent
if (shouldUpRecreateTask(intent) || isTaskRoot)
TaskStackBuilder.create(this).addNextIntentWithParentStack(intent).startActivities() else finish()
}
toolbar.inflateMenu(R.menu.app_manager_menu)
toolbar.setOnMenuItemClickListener(this)
if (!DataStore.proxyApps) {
DataStore.proxyApps = true
DataStore.dirty = true
}
findViewById<Switch>(R.id.onSwitch).setOnCheckedChangeListener { _, checked ->
DataStore.proxyApps = checked
DataStore.dirty = true
finish()
}
bypassSwitch = findViewById(R.id.bypassSwitch)
bypassSwitch.isChecked = DataStore.bypass
bypassSwitch.setOnCheckedChangeListener { _, checked ->
DataStore.bypass = checked
DataStore.dirty = true
}
initProxiedApps()
loadingView = findViewById(R.id.loading)
appListView = findViewById(R.id.list)
appListView.layoutManager = LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false)
appListView.itemAnimator = DefaultItemAnimator()
appListView.adapter = AppsAdapter()
fastScroller = findViewById(R.id.fastscroller)
fastScroller.setRecyclerView(appListView)
instance = this
loadAppsAsync()
}
override fun onMenuItemClick(item: MenuItem): Boolean {
when (item.itemId) {
R.id.action_apply_all -> {
val profiles = ProfileManager.getAllProfiles()
if (profiles != null) {
val proxiedAppString = DataStore.individual
profiles.forEach {
it.individual = proxiedAppString
ProfileManager.updateProfile(it)
}
Toast.makeText(this, R.string.action_apply_all, Toast.LENGTH_SHORT).show()
} else Toast.makeText(this, R.string.action_export_err, Toast.LENGTH_SHORT).show()
return true
}
R.id.action_export -> {
clipboard.primaryClip = ClipData.newPlainText(Key.individual,
"${DataStore.bypass}\n${DataStore.individual}")
Toast.makeText(this, R.string.action_export_msg, Toast.LENGTH_SHORT).show()
return true
}
R.id.action_import -> {
val proxiedAppString = clipboard.primaryClip?.getItemAt(0)?.text?.toString()
if (!proxiedAppString.isNullOrEmpty()) {
val i = proxiedAppString!!.indexOf('\n')
try {
val (enabled, apps) = if (i < 0) Pair(proxiedAppString, "") else
Pair(proxiedAppString.substring(0, i), proxiedAppString.substring(i + 1))
bypassSwitch.isChecked = enabled.toBoolean()
DataStore.individual = apps
DataStore.dirty = true
Toast.makeText(this, R.string.action_import_msg, Toast.LENGTH_SHORT).show()
initProxiedApps(apps)
reloadApps()
return true
} catch (_: IllegalArgumentException) { }
}
Toast.makeText(this, R.string.action_import_err, Toast.LENGTH_SHORT).show()
}
}
return false
}
override fun onKeyUp(keyCode: Int, event: KeyEvent?): Boolean {
return if (keyCode == KeyEvent.KEYCODE_MENU)
if (toolbar.isOverflowMenuShowing) toolbar.hideOverflowMenu() else toolbar.showOverflowMenu()
else super.onKeyUp(keyCode, event)
}
override fun onDestroy() {
instance = null
handler.removeCallbacksAndMessages(null)
super.onDestroy()
}
}
/*******************************************************************************
* *
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
* *
*******************************************************************************/
package com.github.shadowsocks
import android.content.BroadcastReceiver
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import com.github.shadowsocks.App.Companion.app
class BootReceiver : BroadcastReceiver() {
companion object {
private val componentName by lazy { ComponentName(app, BootReceiver::class.java) }
var enabled: Boolean
get() = app.packageManager.getComponentEnabledSetting(componentName) ==
PackageManager.COMPONENT_ENABLED_STATE_ENABLED
set(value) = app.packageManager.setComponentEnabledSetting(componentName,
if (value) PackageManager.COMPONENT_ENABLED_STATE_ENABLED
else PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP)
}
override fun onReceive(context: Context, intent: Intent) {
if (intent.action == Intent.ACTION_BOOT_COMPLETED) app.startService()
}
}
/*******************************************************************************
* *
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
* *
*******************************************************************************/
package com.github.shadowsocks
import android.app.backup.BackupAgentHelper
import android.app.backup.FileBackupHelper
import com.github.shadowsocks.acl.Acl
import com.github.shadowsocks.utils.Key
class ConfigBackupHelper : BackupAgentHelper() {
override fun onCreate() = addHelper("com.github.shadowsocks.database.profile",
FileBackupHelper(this, "../databases/" + Key.PROFILE, Acl.CUSTOM_RULES + ".acl"))
}
/*******************************************************************************
* *
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
* *
*******************************************************************************/
package com.github.shadowsocks
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import com.github.shadowsocks.utils.childFragManager
class GlobalSettingsFragment : ToolbarFragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? =
inflater.inflate(R.layout.layout_global_settings, container, false)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
toolbar.setTitle(R.string.settings)
val fm = childFragmentManager
fm.beginTransaction().replace(R.id.content, GlobalSettingsPreferenceFragment()).commit()
fm.executePendingTransactions()
}
override fun onDetach() {
super.onDetach()
childFragManager = null
}
}
/*******************************************************************************
* *
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
* *
*******************************************************************************/
package com.github.shadowsocks
import android.os.Bundle
import android.support.design.widget.Snackbar
import android.support.v14.preference.SwitchPreference
import android.support.v7.preference.Preference
import com.github.shadowsocks.bg.BaseService
import com.github.shadowsocks.preference.DataStore
import com.github.shadowsocks.utils.Key
import com.github.shadowsocks.utils.TcpFastOpen
import com.takisoft.fix.support.v7.preference.PreferenceFragmentCompatDividers
class GlobalSettingsPreferenceFragment : PreferenceFragmentCompatDividers() {
override fun onCreatePreferencesFix(savedInstanceState: Bundle?, rootKey: String?) {
preferenceManager.preferenceDataStore = DataStore
DataStore.initGlobal()
addPreferencesFromResource(R.xml.pref_global)
val switch = findPreference(Key.isAutoConnect) as SwitchPreference
switch.setOnPreferenceChangeListener { _, value ->
BootReceiver.enabled = value as Boolean
true
}
switch.isChecked = BootReceiver.enabled
val tfo = findPreference(Key.tfo) as SwitchPreference
tfo.isChecked = TcpFastOpen.sendEnabled
tfo.setOnPreferenceChangeListener { _, value ->
val result = TcpFastOpen.enabled(value as Boolean)
if (result != null && result != "Success.")
Snackbar.make(activity!!.findViewById(R.id.snackbar), result, Snackbar.LENGTH_LONG).show()
value == TcpFastOpen.sendEnabled
}
if (!TcpFastOpen.supported) {
tfo.isEnabled = false
tfo.summary = getString(R.string.tcp_fastopen_summary_unsupported, System.getProperty("os.version"))
}
val serviceMode = findPreference(Key.serviceMode)
val portProxy = findPreference(Key.portProxy)
val portLocalDns = findPreference(Key.portLocalDns)
val portTransproxy = findPreference(Key.portTransproxy)
val onServiceModeChange = Preference.OnPreferenceChangeListener { _, newValue ->
val (enabledLocalDns, enabledTransproxy) = when (newValue as String?) {
Key.modeProxy -> Pair(false, false)
Key.modeVpn -> Pair(true, false)
Key.modeTransproxy -> Pair(true, true)
else -> throw IllegalArgumentException()
}
portLocalDns.isEnabled = enabledLocalDns
portTransproxy.isEnabled = enabledTransproxy
true
}
val listener: (Int) -> Unit = {
when (it) {
BaseService.IDLE, BaseService.STOPPED -> {
serviceMode.isEnabled = true
portProxy.isEnabled = true
onServiceModeChange.onPreferenceChange(null, DataStore.serviceMode)
}
else -> {
serviceMode.isEnabled = false
portProxy.isEnabled = false
portLocalDns.isEnabled = false
portTransproxy.isEnabled = false
}
}
}
listener((activity as MainActivity).state)
MainActivity.stateListener = listener
serviceMode.onPreferenceChangeListener = onServiceModeChange
}
override fun onDestroy() {
MainActivity.stateListener = null
super.onDestroy()
}
}
/*******************************************************************************
* *
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
* *
*******************************************************************************/
package com.github.shadowsocks
import android.app.Activity
import android.app.backup.BackupManager
import android.content.ActivityNotFoundException
import android.content.Intent
import android.net.Uri
import android.net.VpnService
import android.nfc.NdefMessage
import android.nfc.NfcAdapter
import android.os.Bundle
import android.os.SystemClock
import android.support.customtabs.CustomTabsIntent
import android.support.design.widget.FloatingActionButton
import android.support.design.widget.Snackbar
import android.support.v4.content.ContextCompat
import android.support.v7.app.AlertDialog
import android.support.v7.app.AppCompatActivity
import android.support.v7.content.res.AppCompatResources
import android.support.v7.preference.PreferenceDataStore
import android.support.v7.widget.TooltipCompat
import android.text.TextUtils
import android.util.Log
import android.view.View
import android.widget.TextView
import com.github.jorgecastilloprz.FABProgressCircle
import com.github.shadowsocks.App.Companion.app
import com.github.shadowsocks.acl.Acl
import com.github.shadowsocks.acl.CustomRulesFragment
import com.github.shadowsocks.aidl.IShadowsocksService
import com.github.shadowsocks.aidl.IShadowsocksServiceCallback
import com.github.shadowsocks.bg.BaseService
import com.github.shadowsocks.bg.Executable
import com.github.shadowsocks.bg.TrafficMonitor
import com.github.shadowsocks.database.Profile
import com.github.shadowsocks.database.ProfileManager
import com.github.shadowsocks.preference.DataStore
import com.github.shadowsocks.preference.OnPreferenceDataStoreChangeListener
import com.github.shadowsocks.utils.Key
import com.github.shadowsocks.utils.responseLength
import com.github.shadowsocks.utils.thread
import com.mikepenz.crossfader.Crossfader
import com.mikepenz.crossfader.view.CrossFadeSlidingPaneLayout
import com.mikepenz.materialdrawer.Drawer
import com.mikepenz.materialdrawer.DrawerBuilder
import com.mikepenz.materialdrawer.interfaces.ICrossfader
import com.mikepenz.materialdrawer.model.PrimaryDrawerItem
import com.mikepenz.materialdrawer.model.interfaces.IDrawerItem
import java.net.HttpURLConnection
import java.net.InetSocketAddress
import java.net.Proxy
import java.net.URL
import java.util.*
class MainActivity : AppCompatActivity(), ShadowsocksConnection.Interface, Drawer.OnDrawerItemClickListener,
OnPreferenceDataStoreChangeListener {
companion object {
private const val TAG = "ShadowsocksMainActivity"
private const val REQUEST_CONNECT = 1
private const val DRAWER_PROFILES = 0L
private const val DRAWER_GLOBAL_SETTINGS = 1L
private const val DRAWER_ABOUT = 3L
private const val DRAWER_FAQ = 4L
private const val DRAWER_CUSTOM_RULES = 5L
var stateListener: ((Int) -> Unit)? = null
}
// UI
private lateinit var fab: FloatingActionButton
private lateinit var fabProgressCircle: FABProgressCircle
internal var crossfader: Crossfader<CrossFadeSlidingPaneLayout>? = null
internal lateinit var drawer: Drawer
private var previousSelectedDrawer: Long = 0 // it's actually lateinit
private var testCount = 0
private lateinit var statusText: TextView
private lateinit var txText: TextView
private lateinit var rxText: TextView
private lateinit var txRateText: TextView
private lateinit var rxRateText: TextView
private val customTabsIntent by lazy {
CustomTabsIntent.Builder()
.setToolbarColor(ContextCompat.getColor(this, R.color.material_primary_500))
.build()
}
fun launchUrl(uri: Uri) = try {
customTabsIntent.launchUrl(this, uri)
} catch (_: ActivityNotFoundException) { } // ignore
fun launchUrl(uri: String) = launchUrl(Uri.parse(uri))
private val greyTint by lazy { ContextCompat.getColorStateList(this, R.color.material_primary_500) }
private val greenTint by lazy { ContextCompat.getColorStateList(this, R.color.material_green_700) }
private fun hideCircle() = try {
fabProgressCircle.hide()
} catch (_: NullPointerException) { } // ignore
// service
var state = BaseService.IDLE
override val serviceCallback: IShadowsocksServiceCallback.Stub by lazy {
object : IShadowsocksServiceCallback.Stub() {
override fun stateChanged(state: Int, profileName: String?, msg: String?) {
app.handler.post { changeState(state, msg) }
}
override fun trafficUpdated(profileId: Int, txRate: Long, rxRate: Long, txTotal: Long, rxTotal: Long) {
app.handler.post { updateTraffic(profileId, txRate, rxRate, txTotal, rxTotal) }
}
override fun trafficPersisted(profileId: Int) {
app.handler.post { ProfilesFragment.instance?.onTrafficPersisted(profileId) }
}
}
}
fun changeState(state: Int, msg: String? = null) {
when (state) {
BaseService.CONNECTING -> {
fab.setImageResource(R.drawable.ic_start_busy)
fabProgressCircle.show()
statusText.setText(R.string.connecting)
}
BaseService.CONNECTED -> {
if (this.state == BaseService.CONNECTING) fabProgressCircle.beginFinalAnimation()
else fabProgressCircle.postDelayed(this::hideCircle, 1000)
fab.setImageResource(R.drawable.ic_start_connected)
statusText.setText(R.string.vpn_connected)
}
BaseService.STOPPING -> {
fab.setImageResource(R.drawable.ic_start_busy)
if (this.state == BaseService.CONNECTED) fabProgressCircle.show() // ignore for stopped
statusText.setText(R.string.stopping)
}
else -> {
fab.setImageResource(R.drawable.ic_start_idle)
fabProgressCircle.postDelayed(this::hideCircle, 1000)
if (msg != null) {
Snackbar.make(findViewById(R.id.snackbar),
getString(R.string.vpn_error).format(Locale.ENGLISH, msg), Snackbar.LENGTH_LONG).show()
Log.e(TAG, "Error to start VPN service: $msg")
}
statusText.setText(R.string.not_connected)
}
}
this.state = state
if (state == BaseService.CONNECTED) {
fab.backgroundTintList = greenTint
TooltipCompat.setTooltipText(fab, getString(R.string.stop))
} else {
fab.backgroundTintList = greyTint
TooltipCompat.setTooltipText(fab, getString(R.string.connect))
updateTraffic(-1, 0, 0, 0, 0)
testCount += 1 // suppress previous test messages
}
ProfilesFragment.instance?.profilesAdapter?.notifyDataSetChanged() // refresh button enabled state
stateListener?.invoke(state)
fab.isEnabled = false
if (state == BaseService.CONNECTED || state == BaseService.STOPPED) app.handler.postDelayed(
{ fab.isEnabled = state == BaseService.CONNECTED || state == BaseService.STOPPED }, 1000)
}
fun updateTraffic(profileId: Int, txRate: Long, rxRate: Long, txTotal: Long, rxTotal: Long) {
txText.text = TrafficMonitor.formatTraffic(txTotal)
rxText.text = TrafficMonitor.formatTraffic(rxTotal)
txRateText.text = getString(R.string.speed, TrafficMonitor.formatTraffic(txRate))
rxRateText.text = getString(R.string.speed, TrafficMonitor.formatTraffic(rxRate))
val child = supportFragmentManager.findFragmentById(R.id.fragment_holder) as ToolbarFragment?
if (state != BaseService.STOPPING)
child?.onTrafficUpdated(profileId, txRate, rxRate, txTotal, rxTotal)
}
/**
* Based on: https://android.googlesource.com/platform/frameworks/base/+/97bfd27/services/core/java/com/android/server/connectivity/NetworkMonitor.java#879
*/
private fun testConnection(id: Int) {
val url = URL("https", when (app.currentProfile!!.route) {
Acl.CHINALIST -> "www.qualcomm.cn"
else -> "www.google.com"
}, "/generate_204")
val conn = (if (BaseService.usingVpnMode) url.openConnection() else
url.openConnection(Proxy(Proxy.Type.SOCKS,
InetSocketAddress("127.0.0.1", DataStore.portProxy))))
as HttpURLConnection
conn.instanceFollowRedirects = false
conn.connectTimeout = 10000
conn.readTimeout = 10000
conn.useCaches = false
val (success, result) = try {
val start = SystemClock.elapsedRealtime()
val code = conn.responseCode
val elapsed = SystemClock.elapsedRealtime() - start
if (code == 204 || code == 200 && conn.responseLength == 0L)
Pair(true, getString(R.string.connection_test_available, elapsed))
else throw Exception(getString(R.string.connection_test_error_status_code, code))
} catch (e: Exception) {
Pair(false, getString(R.string.connection_test_error, e.message))
} finally {
conn.disconnect()
}
if (testCount == id) app.handler.post {
if (success) statusText.text = result else {
statusText.setText(R.string.connection_test_fail)
Snackbar.make(findViewById(R.id.snackbar), result, Snackbar.LENGTH_LONG).show()
}
}
}
override fun onServiceConnected(service: IShadowsocksService) = changeState(service.state)
override fun onServiceDisconnected() = changeState(BaseService.IDLE)
override fun binderDied() {
app.handler.post {
connection.disconnect()
Executable.killAll()
connection.connect()
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (resultCode == Activity.RESULT_OK) app.startService() else {
Snackbar.make(findViewById(R.id.snackbar), R.string.vpn_permission_denied, Snackbar.LENGTH_LONG).show()
Log.e(TAG, "Failed to start VpnService: $data")
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.layout_main)
val drawerBuilder = DrawerBuilder()
.withActivity(this)
.withTranslucentStatusBar(true)
.withHeader(R.layout.layout_header)
.addDrawerItems(
PrimaryDrawerItem()
.withIdentifier(DRAWER_PROFILES)
.withName(R.string.profiles)
.withIcon(AppCompatResources.getDrawable(this, R.drawable.ic_action_description))
.withIconTintingEnabled(true),
PrimaryDrawerItem()
.withIdentifier(DRAWER_CUSTOM_RULES)
.withName(R.string.custom_rules)
.withIcon(AppCompatResources.getDrawable(this, R.drawable.ic_action_assignment))
.withIconTintingEnabled(true),
PrimaryDrawerItem()
.withIdentifier(DRAWER_GLOBAL_SETTINGS)
.withName(R.string.settings)
.withIcon(AppCompatResources.getDrawable(this, R.drawable.ic_action_settings))
.withIconTintingEnabled(true)
)
.addStickyDrawerItems(
PrimaryDrawerItem()
.withIdentifier(DRAWER_FAQ)
.withName(R.string.faq)
.withIcon(AppCompatResources.getDrawable(this, R.drawable.ic_action_help_outline))
.withIconTintingEnabled(true)
.withSelectable(false),
PrimaryDrawerItem()
.withIdentifier(DRAWER_ABOUT)
.withName(R.string.about)
.withIcon(AppCompatResources.getDrawable(this, R.drawable.ic_action_copyright))
.withIconTintingEnabled(true)
)
.withOnDrawerItemClickListener(this)
.withActionBarDrawerToggle(true)
.withSavedInstance(savedInstanceState)
val miniDrawerWidth = resources.getDimension(R.dimen.material_mini_drawer_item)
if (resources.displayMetrics.widthPixels >=
resources.getDimension(R.dimen.profile_item_max_width) + miniDrawerWidth) {
drawer = drawerBuilder.withGenerateMiniDrawer(true).buildView()
val crossfader = Crossfader<CrossFadeSlidingPaneLayout>()
this.crossfader = crossfader
crossfader
.withContent(findViewById(android.R.id.content))
.withFirst(drawer.slider, resources.getDimensionPixelSize(R.dimen.material_drawer_width))
.withSecond(drawer.miniDrawer.build(this), miniDrawerWidth.toInt())
.withSavedInstance(savedInstanceState)
.build()
if (resources.configuration.layoutDirection == View.LAYOUT_DIRECTION_RTL)
crossfader.crossFadeSlidingPaneLayout.setShadowDrawableRight(
AppCompatResources.getDrawable(this, R.drawable.material_drawer_shadow_right))
else crossfader.crossFadeSlidingPaneLayout.setShadowDrawableLeft(
AppCompatResources.getDrawable(this, R.drawable.material_drawer_shadow_left))
drawer.miniDrawer.withCrossFader(object : ICrossfader { // a wrapper is needed
override fun isCrossfaded(): Boolean = crossfader.isCrossFaded
override fun crossfade() = crossfader.crossFade()
})
} else drawer = drawerBuilder.build()
if (savedInstanceState == null) displayFragment(ProfilesFragment())
previousSelectedDrawer = drawer.currentSelection
statusText = findViewById(R.id.status)
txText = findViewById(R.id.tx)
txRateText = findViewById(R.id.txRate)
rxText = findViewById(R.id.rx)
rxRateText = findViewById(R.id.rxRate)
findViewById<View>(R.id.stat).setOnClickListener {
if (state == BaseService.CONNECTED) {
++testCount
statusText.setText(R.string.connection_test_testing)
val id = testCount // it would change by other code
thread { testConnection(id) }
}
}
fab = findViewById(R.id.fab)
fabProgressCircle = findViewById(R.id.fabProgressCircle)
fab.setOnClickListener {
if (state == BaseService.CONNECTED) app.stopService() else thread {
if (BaseService.usingVpnMode) {
val intent = VpnService.prepare(this)
if (intent != null) startActivityForResult(intent, REQUEST_CONNECT)
else app.handler.post { onActivityResult(REQUEST_CONNECT, Activity.RESULT_OK, null) }
} else app.startService()
}
}
changeState(BaseService.IDLE) // reset everything to init state
app.handler.post { connection.connect() }
DataStore.registerChangeListener(this)
val intent = this.intent
if (intent != null) handleShareIntent(intent)
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
handleShareIntent(intent)
}
private fun handleShareIntent(intent: Intent) {
val sharedStr = when (intent.action) {
Intent.ACTION_VIEW -> intent.data.toString()
NfcAdapter.ACTION_NDEF_DISCOVERED -> {
val rawMsgs = intent.getParcelableArrayExtra(NfcAdapter.EXTRA_NDEF_MESSAGES)
if (rawMsgs != null && rawMsgs.isNotEmpty()) String((rawMsgs[0] as NdefMessage).records[0].payload)
else null
}
else -> null
}
if (TextUtils.isEmpty(sharedStr)) return
val profiles = Profile.findAll(sharedStr).toList()
if (profiles.isEmpty()) {
Snackbar.make(findViewById(R.id.snackbar), R.string.profile_invalid_input, Snackbar.LENGTH_LONG).show()
return
}
AlertDialog.Builder(this)
.setTitle(R.string.add_profile_dialog)
.setPositiveButton(R.string.yes, { _, _ -> profiles.forEach { ProfileManager.createProfile(it) } })
.setNegativeButton(R.string.no, null)
.setMessage(profiles.joinToString("\n"))
.create()
.show()
}
override fun onPreferenceDataStoreChanged(store: PreferenceDataStore, key: String?) {
if (key == Key.serviceMode) app.handler.post {
connection.disconnect()
connection.connect()
}
}
private fun displayFragment(fragment: ToolbarFragment) {
supportFragmentManager.beginTransaction().replace(R.id.fragment_holder, fragment).commitAllowingStateLoss()
drawer.closeDrawer()
}
override fun onItemClick(view: View?, position: Int, drawerItem: IDrawerItem<*, *>): Boolean {
val id = drawerItem.identifier
if (id == previousSelectedDrawer) drawer.closeDrawer() else {
previousSelectedDrawer = id
when (id) {
DRAWER_PROFILES -> displayFragment(ProfilesFragment())
DRAWER_GLOBAL_SETTINGS -> displayFragment(GlobalSettingsFragment())
DRAWER_ABOUT -> {
app.track(TAG, "about")
displayFragment(AboutFragment())
}
DRAWER_FAQ -> launchUrl(getString(R.string.faq_url))
DRAWER_CUSTOM_RULES -> displayFragment(CustomRulesFragment())
else -> return false
}
}
return true
}
override fun onResume() {
super.onResume()
app.remoteConfig.fetch()
if (state !in arrayOf(BaseService.STOPPING, BaseService.CONNECTING)) hideCircle()
}
override fun onStart() {
super.onStart()
connection.listeningForBandwidth = true
}
override fun onBackPressed() {
if (drawer.isDrawerOpen) drawer.closeDrawer() else {
val currentFragment = supportFragmentManager.findFragmentById(R.id.fragment_holder) as ToolbarFragment
if (!currentFragment.onBackPressed())
if (currentFragment is ProfilesFragment) super.onBackPressed()
else drawer.setSelection(DRAWER_PROFILES)
}
}
override fun onStop() {
connection.listeningForBandwidth = false
super.onStop()
}
override fun onSaveInstanceState(outState: Bundle?) {
super.onSaveInstanceState(outState)
drawer.saveInstanceState(outState)
crossfader?.saveInstanceState(outState)
}
override fun onDestroy() {
super.onDestroy()
DataStore.unregisterChangeListener(this)
connection.disconnect()
BackupManager(this).dataChanged()
app.handler.removeCallbacksAndMessages(null)
}
}
/*******************************************************************************
* *
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
* *
*******************************************************************************/
package com.github.shadowsocks
import android.app.Activity
import android.content.Intent
import android.os.Bundle
import android.support.v7.app.AlertDialog
import android.support.v7.app.AppCompatActivity
import android.support.v7.widget.Toolbar
import com.github.shadowsocks.plugin.PluginContract
import com.github.shadowsocks.preference.DataStore
class ProfileConfigActivity : AppCompatActivity() {
companion object {
const val REQUEST_CODE_PLUGIN_HELP = 1
}
private val child by lazy { supportFragmentManager.findFragmentById(R.id.content) as ProfileConfigFragment }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.layout_profile_config)
val toolbar = findViewById<Toolbar>(R.id.toolbar)
toolbar.setTitle(R.string.profile_config)
toolbar.setNavigationIcon(R.drawable.ic_navigation_close)
toolbar.setNavigationOnClickListener { onBackPressed() }
toolbar.inflateMenu(R.menu.profile_config_menu)
toolbar.setOnMenuItemClickListener(child)
}
override fun onBackPressed() {
if (DataStore.dirty) AlertDialog.Builder(this)
.setTitle(R.string.unsaved_changes_prompt)
.setPositiveButton(R.string.yes, { _, _ -> child.saveAndExit() })
.setNegativeButton(R.string.no, { _, _ -> finish() })
.setNeutralButton(android.R.string.cancel, null)
.create()
.show() else super.onBackPressed()
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode != REQUEST_CODE_PLUGIN_HELP) super.onActivityResult(requestCode, resultCode, data)
else if (resultCode == Activity.RESULT_OK) AlertDialog.Builder(this)
.setTitle("?")
.setMessage(data?.getCharSequenceExtra(PluginContract.EXTRA_HELP_MESSAGE))
.show()
}
}
/*******************************************************************************
* *
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
* *
*******************************************************************************/
package com.github.shadowsocks
import android.app.Activity
import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.os.UserManager
import android.support.design.widget.Snackbar
import android.support.v14.preference.SwitchPreference
import android.support.v7.app.AlertDialog
import android.support.v7.preference.Preference
import android.support.v7.preference.PreferenceDataStore
import android.support.v7.widget.Toolbar
import android.view.MenuItem
import com.github.shadowsocks.App.Companion.app
import com.github.shadowsocks.bg.BaseService
import com.github.shadowsocks.database.Profile
import com.github.shadowsocks.database.ProfileManager
import com.github.shadowsocks.plugin.PluginConfiguration
import com.github.shadowsocks.plugin.PluginContract
import com.github.shadowsocks.plugin.PluginManager
import com.github.shadowsocks.plugin.PluginOptions
import com.github.shadowsocks.preference.DataStore
import com.github.shadowsocks.preference.IconListPreference
import com.github.shadowsocks.preference.OnPreferenceDataStoreChangeListener
import com.github.shadowsocks.preference.PluginConfigurationDialogFragment
import com.github.shadowsocks.utils.Action
import com.github.shadowsocks.utils.Key
import com.takisoft.fix.support.v7.preference.EditTextPreference
import com.takisoft.fix.support.v7.preference.PreferenceFragmentCompatDividers
class ProfileConfigFragment : PreferenceFragmentCompatDividers(), Toolbar.OnMenuItemClickListener,
Preference.OnPreferenceChangeListener, OnPreferenceDataStoreChangeListener {
companion object {
private const val REQUEST_CODE_PLUGIN_CONFIGURE = 1
}
private lateinit var profile: Profile
private lateinit var isProxyApps: SwitchPreference
private lateinit var plugin: IconListPreference
private lateinit var pluginConfigure: EditTextPreference
private lateinit var pluginConfiguration: PluginConfiguration
override fun onCreatePreferencesFix(savedInstanceState: Bundle?, rootKey: String?) {
preferenceManager.preferenceDataStore = DataStore
val activity = activity!!
val profile = ProfileManager.getProfile(activity.intent.getIntExtra(Action.EXTRA_PROFILE_ID, -1))
if (profile == null) {
activity.finish()
return
}
this.profile = profile
profile.serialize()
addPreferencesFromResource(R.xml.pref_profile)
if (Build.VERSION.SDK_INT >= 25 && activity.getSystemService(UserManager::class.java).isDemoUser) {
findPreference(Key.host).summary = "shadowsocks.example.org"
findPreference(Key.remotePort).summary = "1337"
findPreference(Key.password).summary = "\u2022".repeat(32)
}
isProxyApps = findPreference(Key.proxyApps) as SwitchPreference
if (Build.VERSION.SDK_INT < 21) isProxyApps.parent!!.removePreference(isProxyApps) else {
isProxyApps.isEnabled = BaseService.usingVpnMode
isProxyApps.setOnPreferenceClickListener {
startActivity(Intent(activity, AppManager::class.java))
isProxyApps.isChecked = true
false
}
}
plugin = findPreference(Key.plugin) as IconListPreference
pluginConfigure = findPreference(Key.pluginConfigure) as EditTextPreference
plugin.unknownValueSummary = getString(R.string.plugin_unknown)
plugin.setOnPreferenceChangeListener { _, newValue ->
pluginConfiguration = PluginConfiguration(pluginConfiguration.pluginsOptions, newValue as String)
DataStore.plugin = pluginConfiguration.toString()
DataStore.dirty = true
pluginConfigure.isEnabled = newValue.isNotEmpty()
pluginConfigure.text = pluginConfiguration.selectedOptions.toString()
if (PluginManager.fetchPlugins()[newValue]?.trusted == false)
Snackbar.make(view!!, R.string.plugin_untrusted, Snackbar.LENGTH_LONG).show()
true
}
pluginConfigure.onPreferenceChangeListener = this
initPlugins()
app.listenForPackageChanges { initPlugins() }
DataStore.registerChangeListener(this)
}
private fun initPlugins() {
val plugins = PluginManager.fetchPlugins()
plugin.entries = plugins.map { it.value.label }.toTypedArray()
plugin.entryValues = plugins.map { it.value.id }.toTypedArray()
plugin.entryIcons = plugins.map { it.value.icon }.toTypedArray()
plugin.entryPackageNames = plugins.map { it.value.packageName }.toTypedArray()
pluginConfiguration = PluginConfiguration(DataStore.plugin)
plugin.value = pluginConfiguration.selected
plugin.init()
plugin.checkSummary()
pluginConfigure.isEnabled = pluginConfiguration.selected.isNotEmpty()
pluginConfigure.text = pluginConfiguration.selectedOptions.toString()
}
private fun showPluginEditor() {
val bundle = Bundle()
bundle.putString("key", Key.pluginConfigure)
bundle.putString(PluginConfigurationDialogFragment.PLUGIN_ID_FRAGMENT_TAG, pluginConfiguration.selected)
displayPreferenceDialog(PluginConfigurationDialogFragment(), Key.pluginConfigure, bundle)
}
fun saveAndExit() {
profile.deserialize()
ProfileManager.updateProfile(profile)
ProfilesFragment.instance?.profilesAdapter?.deepRefreshId(profile.id)
activity!!.finish()
}
override fun onResume() {
super.onResume()
isProxyApps.isChecked = DataStore.proxyApps // fetch proxyApps updated by AppManager
}
override fun onPreferenceChange(preference: Preference?, newValue: Any?): Boolean = try {
val selected = pluginConfiguration.selected
pluginConfiguration = PluginConfiguration(pluginConfiguration.pluginsOptions +
(pluginConfiguration.selected to PluginOptions(selected, newValue as String?)), selected)
DataStore.plugin = pluginConfiguration.toString()
DataStore.dirty = true
true
} catch (exc: IllegalArgumentException) {
Snackbar.make(view!!, exc.localizedMessage, Snackbar.LENGTH_LONG).show()
false
}
override fun onPreferenceDataStoreChanged(store: PreferenceDataStore, key: String?) {
if (key != Key.proxyApps && findPreference(key) != null) DataStore.dirty = true
}
override fun onDisplayPreferenceDialog(preference: Preference) {
if (preference.key == Key.pluginConfigure) {
val intent = PluginManager.buildIntent(pluginConfiguration.selected, PluginContract.ACTION_CONFIGURE)
if (intent.resolveActivity(activity!!.packageManager) != null)
startActivityForResult(intent.putExtra(PluginContract.EXTRA_OPTIONS,
pluginConfiguration.selectedOptions.toString()), REQUEST_CODE_PLUGIN_CONFIGURE) else {
showPluginEditor()
}
} else super.onDisplayPreferenceDialog(preference)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode == REQUEST_CODE_PLUGIN_CONFIGURE) when (resultCode) {
Activity.RESULT_OK -> {
val options = data?.getStringExtra(PluginContract.EXTRA_OPTIONS)
pluginConfigure.text = options
onPreferenceChange(null, options)
}
PluginContract.RESULT_FALLBACK -> showPluginEditor()
} else super.onActivityResult(requestCode, resultCode, data)
}
override fun onMenuItemClick(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.action_delete -> {
val activity = activity!!
AlertDialog.Builder(activity)
.setTitle(R.string.delete_confirm_prompt)
.setPositiveButton(R.string.yes, { _, _ ->
ProfileManager.delProfile(profile.id)
activity.finish()
})
.setNegativeButton(R.string.no, null)
.create()
.show()
true
}
R.id.action_apply -> {
saveAndExit()
true
}
else -> false
}
}
override fun onDestroy() {
DataStore.unregisterChangeListener(this)
super.onDestroy()
}
}
/*******************************************************************************
* *
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
* *
*******************************************************************************/
package com.github.shadowsocks
import android.annotation.SuppressLint
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.content.Intent
import android.nfc.NdefMessage
import android.nfc.NdefRecord
import android.nfc.NfcAdapter
import android.os.Bundle
import android.support.design.widget.Snackbar
import android.support.v4.app.DialogFragment
import android.support.v7.widget.*
import android.support.v7.widget.helper.ItemTouchHelper
import android.view.*
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import android.widget.Toast
import com.github.shadowsocks.App.Companion.app
import com.github.shadowsocks.bg.BaseService
import com.github.shadowsocks.bg.TrafficMonitor
import com.github.shadowsocks.database.Profile
import com.github.shadowsocks.database.ProfileManager
import com.github.shadowsocks.plugin.PluginConfiguration
import com.github.shadowsocks.preference.DataStore
import com.github.shadowsocks.utils.Action
import com.github.shadowsocks.widget.UndoSnackbarManager
import com.google.android.gms.ads.AdRequest
import com.google.android.gms.ads.AdSize
import com.google.android.gms.ads.AdView
import net.glxn.qrgen.android.QRCode
class ProfilesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener {
companion object {
/**
* used for callback from ProfileManager and stateChanged from MainActivity
*/
var instance: ProfilesFragment? = null
private const val KEY_URL = "com.github.shadowsocks.QRCodeDialog.KEY_URL"
}
/**
* Is ProfilesFragment editable at all.
*/
private val isEnabled get() = when ((activity as MainActivity).state) {
BaseService.CONNECTED, BaseService.STOPPED -> true
else -> false
}
private fun isProfileEditable(id: Int) = when ((activity as MainActivity).state) {
BaseService.CONNECTED -> id != DataStore.profileId
BaseService.STOPPED -> true
else -> false
}
@SuppressLint("ValidFragment")
private class QRCodeDialog() : DialogFragment() {
constructor(url: String) : this() {
val bundle = Bundle()
bundle.putString(KEY_URL, url)
arguments = bundle
}
private val url get() = arguments!!.getString(KEY_URL)
private val nfcShareItem by lazy { url.toByteArray() }
private var adapter: NfcAdapter? = null
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val image = ImageView(context)
image.layoutParams = LinearLayout.LayoutParams(-1, -1)
val size = resources.getDimensionPixelSize(R.dimen.qr_code_size)
image.setImageBitmap((QRCode.from(url).withSize(size, size) as QRCode).bitmap())
return image
}
override fun onAttach(context: Context?) {
super.onAttach(context)
val adapter = NfcAdapter.getDefaultAdapter(context)
adapter?.setNdefPushMessage(NdefMessage(arrayOf(
NdefRecord(NdefRecord.TNF_ABSOLUTE_URI, nfcShareItem, byteArrayOf(), nfcShareItem))), activity)
this.adapter = adapter
}
override fun onDetach() {
adapter?.setNdefPushMessage(null, activity)
adapter = null
super.onDetach()
}
}
inner class ProfileViewHolder(view: View) : RecyclerView.ViewHolder(view),
View.OnClickListener, PopupMenu.OnMenuItemClickListener {
internal lateinit var item: Profile
private val text1 = itemView.findViewById<TextView>(android.R.id.text1)
private val text2 = itemView.findViewById<TextView>(android.R.id.text2)
private val traffic = itemView.findViewById<TextView>(R.id.traffic)
private val edit = itemView.findViewById<View>(R.id.edit)
private var adView: AdView? = null
init {
edit.setOnClickListener { startConfig(item.id) }
TooltipCompat.setTooltipText(edit, edit.contentDescription)
itemView.setOnClickListener(this)
val share = itemView.findViewById<View>(R.id.share)
share.setOnClickListener {
val popup = PopupMenu(activity!!, share)
popup.menuInflater.inflate(R.menu.profile_share_popup, popup.menu)
popup.setOnMenuItemClickListener(this)
popup.show()
}
TooltipCompat.setTooltipText(share, share.contentDescription)
}
fun bind(item: Profile) {
this.item = item
val editable = isProfileEditable(item.id)
edit.isEnabled = editable
edit.alpha = if (editable) 1F else .5F
var tx = item.tx
var rx = item.rx
if (item.id == bandwidthProfile) {
tx += txTotal
rx += rxTotal
}
text1.text = item.formattedName
val t2 = ArrayList<String>()
if (!item.name.isNullOrEmpty()) t2 += item.formattedAddress
val id = PluginConfiguration(item.plugin ?: "").selected
if (id.isNotEmpty()) t2 += app.getString(R.string.profile_plugin, id)
if (t2.isEmpty()) text2.visibility = View.GONE else {
text2.visibility = View.VISIBLE
text2.text = t2.joinToString("\n")
}
if (tx <= 0 && rx <= 0) traffic.visibility = View.GONE else {
traffic.visibility = View.VISIBLE
traffic.text = getString(R.string.traffic,
TrafficMonitor.formatTraffic(tx), TrafficMonitor.formatTraffic(rx))
}
if (item.id == DataStore.profileId) {
itemView.isSelected = true
selectedItem = this
} else {
itemView.isSelected = false
if (selectedItem === this) selectedItem = null
}
var adView = adView
if (item.host == "198.199.101.152") {
if (adView == null) {
val params =
LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)
params.gravity = Gravity.CENTER_HORIZONTAL
adView = AdView(activity)
adView.layoutParams = params
adView.adUnitId = "ca-app-pub-9097031975646651/7760346322"
adView.adSize = AdSize.FLUID
itemView.findViewById<LinearLayout>(R.id.content).addView(adView)
// Load Ad
val adBuilder = AdRequest.Builder()
adBuilder.addTestDevice("B08FC1764A7B250E91EA9D0D5EBEB208")
adView.loadAd(adBuilder.build())
this.adView = adView
} else adView.visibility = View.VISIBLE
} else if (adView != null) adView.visibility = View.GONE
}
override fun onClick(v: View?) {
if (isEnabled) {
val activity = activity as MainActivity
val old = DataStore.profileId
app.switchProfile(item.id)
profilesAdapter.refreshId(old)
itemView.isSelected = true
if (activity.state == BaseService.CONNECTED) app.reloadService()
}
}
override fun onMenuItemClick(item: MenuItem): Boolean = when (item.itemId) {
R.id.action_qr_code_nfc -> {
fragmentManager!!.beginTransaction().add(QRCodeDialog(this.item.toString()), "")
.commitAllowingStateLoss()
true
}
R.id.action_export -> {
clipboard.primaryClip = ClipData.newPlainText(null, this.item.toString())
true
}
else -> false
}
}
inner class ProfilesAdapter : RecyclerView.Adapter<ProfileViewHolder>() {
internal val profiles = ProfileManager.getAllProfiles()?.toMutableList() ?: mutableListOf()
init {
setHasStableIds(true) // see: http://stackoverflow.com/a/32488059/2245107
}
override fun onBindViewHolder(holder: ProfileViewHolder, position: Int) = holder.bind(profiles[position])
override fun onCreateViewHolder(parent: ViewGroup?, viewType: Int): ProfileViewHolder = ProfileViewHolder(
LayoutInflater.from(parent!!.context).inflate(R.layout.layout_profile, parent, false))
override fun getItemCount(): Int = profiles.size
override fun getItemId(position: Int): Long = profiles[position].id.toLong()
fun add(item: Profile) {
undoManager.flush()
val pos = itemCount
profiles += item
notifyItemInserted(pos)
}
fun move(from: Int, to: Int) {
undoManager.flush()
val first = profiles[from]
var previousOrder = first.userOrder
val (step, range) = if (from < to) Pair(1, from until to) else Pair(-1, to + 1 downTo from)
for (i in range) {
val next = profiles[i + step]
val order = next.userOrder
next.userOrder = previousOrder
previousOrder = order
profiles[i] = next
ProfileManager.updateProfile(next)
}
first.userOrder = previousOrder
profiles[to] = first
ProfileManager.updateProfile(first)
notifyItemMoved(from, to)
}
fun remove(pos: Int) {
profiles.removeAt(pos)
notifyItemRemoved(pos)
}
fun undo(actions: List<Pair<Int, Profile>>) {
for ((index, item) in actions) {
profiles.add(index, item)
notifyItemInserted(index)
}
}
fun commit(actions: List<Pair<Int, Profile>>) {
for ((_, item) in actions) ProfileManager.delProfile(item.id)
}
fun refreshId(id: Int) {
val index = profiles.indexOfFirst { it.id == id }
if (index >= 0) notifyItemChanged(index)
}
fun deepRefreshId(id: Int) {
val index = profiles.indexOfFirst { it.id == id }
if (index < 0) return
profiles[index] = ProfileManager.getProfile(id)!!
notifyItemChanged(index)
}
fun removeId(id: Int) {
val index = profiles.indexOfFirst { it.id == id }
if (index < 0) return
profiles.removeAt(index)
notifyItemRemoved(index)
if (id == DataStore.profileId) DataStore.profileId = 0 // switch to null profile
}
}
private var selectedItem: ProfileViewHolder? = null
val profilesAdapter by lazy { ProfilesAdapter() }
private lateinit var undoManager: UndoSnackbarManager<Profile>
private var bandwidthProfile: Int = 0
private var txTotal: Long = 0L
private var rxTotal: Long = 0L
private val clipboard by lazy { activity!!.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager }
private fun startConfig(id: Int) = startActivity(Intent(context, ProfileConfigActivity::class.java)
.putExtra(Action.EXTRA_PROFILE_ID, id))
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? =
inflater.inflate(R.layout.layout_list, container, false)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
toolbar.setTitle(R.string.profiles)
toolbar.inflateMenu(R.menu.profile_manager_menu)
toolbar.setOnMenuItemClickListener(this)
if (ProfileManager.getFirstProfile() == null)
DataStore.profileId = ProfileManager.createProfile().id
val profilesList = view.findViewById<RecyclerView>(R.id.list)
val layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
profilesList.layoutManager = layoutManager
profilesList.addItemDecoration(DividerItemDecoration(context, layoutManager.orientation))
layoutManager.scrollToPosition(profilesAdapter.profiles.indexOfFirst { it.id == DataStore.profileId })
val animator = DefaultItemAnimator()
animator.supportsChangeAnimations = false // prevent fading-in/out when rebinding
profilesList.itemAnimator = animator
profilesList.adapter = profilesAdapter
instance = this
undoManager = UndoSnackbarManager(activity!!.findViewById(R.id.snackbar),
profilesAdapter::undo, profilesAdapter::commit)
ItemTouchHelper(object : ItemTouchHelper.SimpleCallback(ItemTouchHelper.UP or ItemTouchHelper.DOWN,
ItemTouchHelper.START or ItemTouchHelper.END) {
override fun getSwipeDirs(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder): Int =
if (isProfileEditable((viewHolder as ProfileViewHolder).item.id))
super.getSwipeDirs(recyclerView, viewHolder) else 0
override fun getDragDirs(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder): Int =
if (isEnabled) super.getDragDirs(recyclerView, viewHolder) else 0
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
val index = viewHolder.adapterPosition
profilesAdapter.remove(index)
undoManager.remove(Pair(index, (viewHolder as ProfileViewHolder).item))
}
override fun onMove(recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder): Boolean {
profilesAdapter.move(viewHolder.adapterPosition, target.adapterPosition)
return true
}
}).attachToRecyclerView(profilesList)
}
override fun onMenuItemClick(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.action_scan_qr_code -> {
startActivity(Intent(context, ScannerActivity::class.java))
true
}
R.id.action_import -> {
try {
val profiles = Profile.findAll(clipboard.primaryClip.getItemAt(0).text).toList()
if (profiles.isNotEmpty()) {
profiles.forEach { ProfileManager.createProfile(it) }
Toast.makeText(activity, R.string.action_import_msg, Toast.LENGTH_SHORT).show()
return true
}
} catch (exc: Exception) {
app.track(exc)
}
Snackbar.make(activity!!.findViewById(R.id.snackbar), R.string.action_import_err, Snackbar.LENGTH_LONG)
.show()
true
}
R.id.action_manual_settings -> {
startConfig(ProfileManager.createProfile().id)
true
}
R.id.action_export -> {
val profiles = ProfileManager.getAllProfiles()
if (profiles != null) {
clipboard.primaryClip = ClipData.newPlainText(null, profiles.joinToString("\n"))
Toast.makeText(activity, R.string.action_export_msg, Toast.LENGTH_SHORT).show()
} else Toast.makeText(activity, R.string.action_export_err, Toast.LENGTH_SHORT).show()
true
}
else -> false
}
}
override fun onTrafficUpdated(profileId: Int, txRate: Long, rxRate: Long, txTotal: Long, rxTotal: Long) {
if (profileId != -1) { // ignore resets from MainActivity
if (bandwidthProfile != profileId) {
onTrafficPersisted(bandwidthProfile)
bandwidthProfile = profileId
}
this.txTotal = txTotal
this.rxTotal = rxTotal
profilesAdapter.refreshId(profileId)
}
}
fun onTrafficPersisted(profileId: Int) {
txTotal = 0
rxTotal = 0
if (bandwidthProfile != profileId) {
onTrafficPersisted(bandwidthProfile)
bandwidthProfile = profileId
}
profilesAdapter.deepRefreshId(profileId)
}
override fun onDetach() {
undoManager.flush()
super.onDetach()
}
override fun onDestroy() {
instance = null
super.onDestroy()
}
}
/*******************************************************************************
* *
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
* *
*******************************************************************************/
package com.github.shadowsocks
import android.app.Activity
import android.content.Intent
import android.content.pm.ShortcutManager
import android.os.Build
import android.os.Bundle
import android.os.PersistableBundle
import android.support.v4.content.pm.ShortcutInfoCompat
import android.support.v4.content.pm.ShortcutManagerCompat
import android.support.v4.graphics.drawable.IconCompat
import com.github.shadowsocks.App.Companion.app
import com.github.shadowsocks.aidl.IShadowsocksService
import com.github.shadowsocks.bg.BaseService
class QuickToggleShortcut : Activity(), ShadowsocksConnection.Interface {
override fun onCreate(savedInstanceState: Bundle?, persistentState: PersistableBundle?) {
super.onCreate(savedInstanceState, persistentState)
if (intent.action == Intent.ACTION_CREATE_SHORTCUT) {
setResult(Activity.RESULT_OK, ShortcutManagerCompat.createShortcutResultIntent(this,
ShortcutInfoCompat.Builder(this, "toggle")
.setIntent(Intent(this, QuickToggleShortcut::class.java).setAction(Intent.ACTION_MAIN))
.setIcon(IconCompat.createWithResource(this, R.drawable.ic_qu_shadowsocks_launcher))
.setShortLabel(getString(R.string.quick_toggle))
.build()))
finish()
} else {
connection.connect()
if (Build.VERSION.SDK_INT >= 25) getSystemService(ShortcutManager::class.java).reportShortcutUsed("toggle")
}
}
override fun onServiceConnected(service: IShadowsocksService) {
when (service.state) {
BaseService.STOPPED -> app.startService()
BaseService.CONNECTED -> app.stopService()
}
finish()
}
override fun onDestroy() {
connection.disconnect()
super.onDestroy()
}
}
/*******************************************************************************
* *
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
* *
*******************************************************************************/
package com.github.shadowsocks
import android.content.pm.PackageManager
import android.content.pm.ShortcutManager
import android.os.Build
import android.os.Bundle
import android.support.v4.app.ActivityCompat
import android.support.v4.app.TaskStackBuilder
import android.support.v4.content.ContextCompat
import android.support.v7.app.AppCompatActivity
import android.support.v7.widget.Toolbar
import android.widget.Toast
import com.github.shadowsocks.database.Profile
import com.github.shadowsocks.database.ProfileManager
import com.google.zxing.Result
import me.dm7.barcodescanner.zxing.ZXingScannerView
class ScannerActivity : AppCompatActivity(), ZXingScannerView.ResultHandler {
companion object {
private const val MY_PERMISSIONS_REQUEST_CAMERA = 1
}
private lateinit var scannerView: ZXingScannerView
private fun navigateUp() {
val intent = parentActivityIntent
if (shouldUpRecreateTask(intent) || isTaskRoot)
TaskStackBuilder.create(this).addNextIntentWithParentStack(intent).startActivities()
else finish()
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.layout_scanner)
val toolbar = findViewById<Toolbar>(R.id.toolbar)
toolbar.title = title
toolbar.setNavigationIcon(R.drawable.abc_ic_ab_back_material)
toolbar.setNavigationOnClickListener { navigateUp() }
scannerView = findViewById(R.id.scanner)
if (Build.VERSION.SDK_INT >= 25) getSystemService(ShortcutManager::class.java).reportShortcutUsed("scan")
}
override fun onResume() {
super.onResume()
val permissionCheck = ContextCompat.checkSelfPermission(this,
android.Manifest.permission.CAMERA)
if (permissionCheck == PackageManager.PERMISSION_GRANTED) {
scannerView.setResultHandler(this) // Register ourselves as a handler for scan results.
scannerView.startCamera() // Start camera on resume
} else ActivityCompat.requestPermissions(this, arrayOf(android.Manifest.permission.CAMERA),
MY_PERMISSIONS_REQUEST_CAMERA)
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
if (requestCode == MY_PERMISSIONS_REQUEST_CAMERA)
if (grantResults.getOrNull(0) == PackageManager.PERMISSION_GRANTED) {
scannerView.setResultHandler(this)
scannerView.startCamera()
} else {
Toast.makeText(this, R.string.add_profile_scanner_permission_required, Toast.LENGTH_SHORT).show()
finish()
}
else super.onRequestPermissionsResult(requestCode, permissions, grantResults)
}
override fun onPause() {
super.onPause()
scannerView.stopCamera() // Stop camera on pause
}
override fun handleResult(rawResult: Result?) {
Profile.findAll(rawResult?.text).forEach { ProfileManager.createProfile(it) }
navigateUp()
}
}
/*******************************************************************************
* *
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
* *
*******************************************************************************/
package com.github.shadowsocks
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.ServiceConnection
import android.os.IBinder
import android.os.RemoteException
import com.github.shadowsocks.aidl.IShadowsocksService
import com.github.shadowsocks.aidl.IShadowsocksServiceCallback
import com.github.shadowsocks.bg.BaseService
import com.github.shadowsocks.utils.Action
import java.util.*
class ShadowsocksConnection(private val instance: Interface) : ServiceConnection {
companion object {
private val connections = WeakHashMap<Interface, ShadowsocksConnection>()
}
interface Interface : IBinder.DeathRecipient {
val serviceCallback: IShadowsocksServiceCallback? get() = null
val connection: ShadowsocksConnection
get() = connections.getOrPut(this, { ShadowsocksConnection(this) })
fun onServiceConnected(service: IShadowsocksService) { }
/**
* Different from Android framework, this method will be called even when you call `detachService`.
*/
fun onServiceDisconnected() { }
override fun binderDied() { }
}
private var connectionActive = false
private var callbackRegistered = false
private var binder: IBinder? = null
var listeningForBandwidth = false
set(value) {
val service = service
if (listeningForBandwidth != value && service != null && instance.serviceCallback != null)
if (value) service.startListeningForBandwidth(instance.serviceCallback)
else service.stopListeningForBandwidth(instance.serviceCallback)
field = value
}
var service: IShadowsocksService? = null
override fun onServiceConnected(name: ComponentName?, binder: IBinder) {
this.binder = binder
binder.linkToDeath(instance, 0)
val service = IShadowsocksService.Stub.asInterface(binder)!!
this.service = service
if (instance.serviceCallback != null && !callbackRegistered) try {
service.registerCallback(instance.serviceCallback)
callbackRegistered = true
if (listeningForBandwidth) service.startListeningForBandwidth(instance.serviceCallback)
} catch (_: RemoteException) { }
instance.onServiceConnected(service)
}
override fun onServiceDisconnected(name: ComponentName?) {
unregisterCallback()
instance.onServiceDisconnected()
service = null
binder = null
}
private fun unregisterCallback() {
val service = service
if (service != null && instance.serviceCallback != null && callbackRegistered) try {
service.unregisterCallback(instance.serviceCallback)
} catch (_: RemoteException) { }
callbackRegistered = false
}
fun connect() {
if (connectionActive) return
connectionActive = true
val intent = Intent(instance as Context, BaseService.serviceClass.java).setAction(Action.SERVICE)
instance.bindService(intent, this, Context.BIND_AUTO_CREATE)
}
fun disconnect() {
unregisterCallback()
instance.onServiceDisconnected()
if (connectionActive) try {
(instance as Context).unbindService(this)
} catch (_: IllegalArgumentException) { } // ignore
connectionActive = false
binder?.unlinkToDeath(instance, 0)
binder = null
service = null
}
}
/*******************************************************************************
* *
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
* *
*******************************************************************************/
package com.github.shadowsocks
import android.os.Bundle
import android.support.v4.app.Fragment
import android.support.v7.widget.Toolbar
import android.view.View
/**
* @author Mygod
*/
open class ToolbarFragment : Fragment() {
protected lateinit var toolbar: Toolbar
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
toolbar = view.findViewById(R.id.toolbar)
val activity = activity as MainActivity
if (activity.crossfader == null) activity.drawer.setToolbar(activity, toolbar, true)
}
open fun onTrafficUpdated(profileId: Int, txRate: Long, rxRate: Long, txTotal: Long, rxTotal: Long) { }
open fun onBackPressed(): Boolean = false
}
/*******************************************************************************
* *
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
* *
*******************************************************************************/
package com.github.shadowsocks
import android.app.KeyguardManager
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.net.VpnService
import android.os.Bundle
import android.os.PersistableBundle
import android.support.v7.app.AppCompatActivity
import android.util.Log
import com.github.shadowsocks.App.Companion.app
import com.github.shadowsocks.aidl.IShadowsocksService
import com.github.shadowsocks.bg.BaseService
import com.github.shadowsocks.utils.broadcastReceiver
class VpnRequestActivity : AppCompatActivity(), ShadowsocksConnection.Interface {
companion object {
private const val TAG = "VpnRequestActivity"
private const val REQUEST_CONNECT = 1
}
private var receiver: BroadcastReceiver? = null
override fun onCreate(savedInstanceState: Bundle?, persistentState: PersistableBundle?) {
super.onCreate(savedInstanceState, persistentState)
if (!BaseService.usingVpnMode) {
finish()
return
}
val km = getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager
if (km.inKeyguardRestrictedInputMode()) {
receiver = broadcastReceiver { _, _ -> connection.connect() }
registerReceiver(receiver, IntentFilter(Intent.ACTION_USER_PRESENT))
} else connection.connect()
}
override fun onServiceConnected(service: IShadowsocksService) {
app.handler.postDelayed({
val intent = VpnService.prepare(this)
if (intent == null) onActivityResult(REQUEST_CONNECT, RESULT_OK, null)
else startActivityForResult(intent, REQUEST_CONNECT)
}, 1000)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (resultCode == RESULT_OK) app.startService() else Log.e(TAG, "Failed to start VpnService")
finish()
}
override fun onDestroy() {
super.onDestroy()
connection.disconnect()
if (receiver != null) unregisterReceiver(receiver)
}
}
/*******************************************************************************
* *
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
* *
*******************************************************************************/
package com.github.shadowsocks.acl
import android.support.v7.util.SortedList
import android.util.Log
import com.github.shadowsocks.App.Companion.app
import com.github.shadowsocks.utils.Subnet
import com.github.shadowsocks.utils.asIterable
import com.j256.ormlite.field.DatabaseField
import java.io.File
import java.io.FileNotFoundException
import java.io.Reader
import java.net.URL
class Acl {
companion object {
const val TAG = "Acl"
const val ALL = "all"
const val BYPASS_LAN = "bypass-lan"
const val BYPASS_CHN = "bypass-china"
const val BYPASS_LAN_CHN = "bypass-lan-china"
const val GFWLIST = "gfwlist"
const val CHINALIST = "china-list"
const val CUSTOM_RULES = "custom-rules"
const val CUSTOM_RULES_FLATTENED = "custom-rules-flattened"
val networkAclParser = "^IMPORT_URL\\s*<(.+)>\\s*$".toRegex()
fun getFile(id: String) = File(app.filesDir, id + ".acl")
val customRules: Acl get() {
val acl = Acl()
try {
acl.fromId(CUSTOM_RULES)
} catch (_: FileNotFoundException) { }
acl.bypass = true
acl.bypassHostnames.clear() // everything is bypassed
return acl
}
fun save(id: String, acl: Acl): Unit = getFile(id).bufferedWriter().use { it.write(acl.toString()) }
}
private abstract class BaseSorter<T> : SortedList.Callback<T>() {
override fun onInserted(position: Int, count: Int) { }
override fun areContentsTheSame(oldItem: T?, newItem: T?): Boolean = oldItem == newItem
override fun onMoved(fromPosition: Int, toPosition: Int) { }
override fun onChanged(position: Int, count: Int) { }
override fun onRemoved(position: Int, count: Int) { }
override fun areItemsTheSame(item1: T?, item2: T?): Boolean = item1 == item2
override fun compare(o1: T?, o2: T?): Int =
if (o1 == null) if (o2 == null) 0 else 1 else if (o2 == null) -1 else compareNonNull(o1, o2)
abstract fun compareNonNull(o1: T, o2: T): Int
}
private open class DefaultSorter<T : Comparable<T>> : BaseSorter<T>() {
override fun compareNonNull(o1: T, o2: T): Int = o1.compareTo(o2)
}
private object StringSorter : DefaultSorter<String>()
private object SubnetSorter : DefaultSorter<Subnet>()
private object URLSorter : BaseSorter<URL>() {
private val ordering = compareBy<URL>({ it.host }, { it.port }, { it.file }, { it.protocol })
override fun compareNonNull(o1: URL, o2: URL): Int = ordering.compare(o1, o2)
}
@DatabaseField(generatedId = true)
var id = 0
val bypassHostnames = SortedList(String::class.java, StringSorter)
val proxyHostnames = SortedList(String::class.java, StringSorter)
val subnets = SortedList(Subnet::class.java, SubnetSorter)
val urls = SortedList(URL::class.java, URLSorter)
@DatabaseField
var bypass = false
fun fromAcl(other: Acl): Acl {
bypassHostnames.clear()
for (item in other.bypassHostnames.asIterable()) bypassHostnames.add(item)
proxyHostnames.clear()
for (item in other.proxyHostnames.asIterable()) proxyHostnames.add(item)
subnets.clear()
for (item in other.subnets.asIterable()) subnets.add(item)
urls.clear()
for (item in other.urls.asIterable()) urls.add(item)
bypass = other.bypass
return this
}
fun fromReader(reader: Reader, defaultBypass: Boolean = false): Acl {
bypassHostnames.clear()
proxyHostnames.clear()
subnets.clear()
urls.clear()
bypass = defaultBypass
val bypassSubnets by lazy { SortedList(Subnet::class.java, SubnetSorter) }
val proxySubnets by lazy { SortedList(Subnet::class.java, SubnetSorter) }
var hostnames: SortedList<String>? = if (defaultBypass) proxyHostnames else bypassHostnames
var subnets: SortedList<Subnet>? = if (defaultBypass) proxySubnets else bypassSubnets
reader.useLines {
for (line in it) {
val blocks = line.split('#', limit = 2)
val url = networkAclParser.matchEntire(blocks.getOrElse(1, {""}))?.groupValues?.getOrNull(1)
if (url != null) urls.add(URL(url))
val input = blocks[0].trim()
when (input) {
"[outbound_block_list]" -> {
hostnames = null
subnets = null
}
"[black_list]", "[bypass_list]" -> {
hostnames = bypassHostnames
subnets = bypassSubnets
}
"[white_list]", "[proxy_list]" -> {
hostnames = proxyHostnames
subnets = proxySubnets
}
"[reject_all]", "[bypass_all]" -> bypass = true
"[accept_all]", "[proxy_all]" -> bypass = false
else -> if (subnets != null && input.isNotEmpty()) try {
subnets!!.add(Subnet.fromString(input))
} catch (_: IllegalArgumentException) {
hostnames!!.add(input)
}
}
}
}
for (item in (if (bypass) proxySubnets else bypassSubnets).asIterable()) this.subnets.add(item)
return this
}
fun fromId(id: String): Acl = fromReader(Acl.getFile(id).bufferedReader())
fun flatten(depth: Int): Acl {
if (depth > 0) for (url in urls.asIterable()) {
val child = Acl().fromReader(url.openStream().bufferedReader(), bypass).flatten(depth - 1)
if (bypass != child.bypass) {
Log.w(TAG, "Imported network ACL has a conflicting mode set. " +
"This will probably not work as intended. URL: $url")
child.subnets.clear() // subnets for the different mode are discarded
child.bypass = bypass
}
for (item in child.bypassHostnames.asIterable()) bypassHostnames.add(item)
for (item in child.proxyHostnames.asIterable()) proxyHostnames.add(item)
for (item in child.subnets.asIterable()) subnets.add(item)
}
urls.clear()
return this
}
override fun toString(): String {
val result = StringBuilder()
result.append(if (bypass) "[bypass_all]\n" else "[proxy_all]\n")
val bypassList = (if (bypass) bypassHostnames.asIterable().asSequence() else
subnets.asIterable().asSequence().map(Subnet::toString) + proxyHostnames.asIterable().asSequence()).toList()
val proxyList = (if (bypass) subnets.asIterable().asSequence().map(Subnet::toString) +
proxyHostnames.asIterable().asSequence() else bypassHostnames.asIterable().asSequence()).toList()
if (bypassList.isNotEmpty()) {
result.append("[bypass_list]\n")
result.append(bypassList.joinToString("\n"))
result.append('\n')
}
if (proxyList.isNotEmpty()) {
result.append("[proxy_list]\n")
result.append(proxyList.joinToString("\n"))
result.append('\n')
}
result.append(urls.asIterable().joinToString("") { "#IMPORT_URL <$it>\n" })
return result.toString()
}
}
/*******************************************************************************
* *
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
* *
*******************************************************************************/
package com.github.shadowsocks.acl
import android.util.Log
import com.evernote.android.job.Job
import com.evernote.android.job.JobCreator
import com.evernote.android.job.JobRequest
import java.io.IOException
import java.net.URL
import java.util.concurrent.TimeUnit
class AclSyncJob(private val route: String) : Job() {
companion object : JobCreator {
private const val TAG = "AclSyncJob"
private const val MAX_RESCHEDULE = 3
override fun create(tag: String): AclSyncJob? {
val parts = tag.split(':', limit = 2)
return when (parts[0]) {
TAG -> AclSyncJob(parts[1])
else -> {
Log.w(TAG, "Unknown job tag: " + tag)
null
}
}
}
fun schedule(route: String): Int = JobRequest.Builder(TAG + ':' + route)
.setExecutionWindow(TimeUnit.SECONDS.toMillis(10), TimeUnit.DAYS.toMillis(28))
.setRequirementsEnforced(true)
.setRequiredNetworkType(JobRequest.NetworkType.UNMETERED)
.setRequiresCharging(true)
.setUpdateCurrent(true)
.build().schedule()
}
override fun onRunJob(params: Params): Result = try {
val acl = URL("https://shadowsocks.org/acl/android/v1/$route.acl").openStream().bufferedReader()
.use { it.readText() }
Acl.getFile(route).printWriter().use { it.write(acl) }
Result.SUCCESS
} catch (e: IOException) {
e.printStackTrace()
if (params.failureCount < AclSyncJob.MAX_RESCHEDULE) Result.RESCHEDULE else Result.FAILURE
} catch (e: Exception) { // unknown failures, probably shouldn't retry
e.printStackTrace()
Result.FAILURE
}
}
/*******************************************************************************
* *
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
* *
*******************************************************************************/
package com.github.shadowsocks.acl
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.os.Bundle
import android.support.design.widget.Snackbar
import android.support.v7.app.AlertDialog
import android.support.v7.widget.DefaultItemAnimator
import android.support.v7.widget.LinearLayoutManager
import android.support.v7.widget.RecyclerView
import android.support.v7.widget.Toolbar
import android.support.v7.widget.helper.ItemTouchHelper
import android.view.LayoutInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.widget.EditText
import android.widget.Spinner
import android.widget.TextView
import android.widget.Toast
import com.futuremind.recyclerviewfastscroll.FastScroller
import com.futuremind.recyclerviewfastscroll.SectionTitleProvider
import com.github.shadowsocks.App.Companion.app
import com.github.shadowsocks.MainActivity
import com.github.shadowsocks.R
import com.github.shadowsocks.ToolbarFragment
import com.github.shadowsocks.bg.BaseService
import com.github.shadowsocks.utils.Subnet
import com.github.shadowsocks.utils.asIterable
import com.github.shadowsocks.widget.UndoSnackbarManager
import java.net.IDN
import java.net.URL
import java.util.*
class CustomRulesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener {
companion object {
private const val TEMPLATE_REGEX_DOMAIN = "(^|\\.)%s$"
private const val SELECTED_SUBNETS = "com.github.shadowsocks.acl.CustomRulesFragment.SELECTED_SUBNETS"
private const val SELECTED_HOSTNAMES = "com.github.shadowsocks.acl.CustomRulesFragment.SELECTED_HOSTNAMES"
private const val SELECTED_URLS = "com.github.shadowsocks.acl.CustomRulesFragment.SELECTED_URLS"
private val PATTERN_DOMAIN = """(?<=^\(\^\|\\\.\)).*(?=\$$)""".toRegex()
}
private enum class Template {
Generic,
Domain,
Url;
}
private inner class AclRuleViewHolder(view: View) : RecyclerView.ViewHolder(view),
View.OnClickListener, View.OnLongClickListener {
lateinit var item: Any
private val text = view.findViewById<TextView>(android.R.id.text1)
init {
view.setPaddingRelative(view.paddingStart, view.paddingTop,
Math.max(view.paddingEnd, resources.getDimensionPixelSize(R.dimen.fastscroll__bubble_corner)),
view.paddingBottom)
view.setOnClickListener(this)
view.setOnLongClickListener(this)
view.setBackgroundResource(R.drawable.background_selectable)
}
fun bind(hostname: String) {
item = hostname
text.text = hostname
itemView.isSelected = selectedItems.contains(hostname)
}
fun bind(subnet: Subnet) {
item = subnet
text.text = subnet.toString()
itemView.isSelected = selectedItems.contains(subnet)
}
fun bind(url: URL) {
item = url
text.text = url.toString()
itemView.isSelected = selectedItems.contains(url)
}
override fun onClick(v: View?) {
if (selectedItems.isNotEmpty()) onLongClick(v) else {
val (templateSelector, editText, dialog) = createAclRuleDialog(item)
dialog
.setNeutralButton(R.string.delete, { _, _ ->
adapter.remove(item)
undoManager.remove(Pair(-1, item))
})
.setPositiveButton(android.R.string.ok, { _, _ ->
adapter.remove(item)
val index = adapter.addFromTemplate(templateSelector.selectedItemPosition,
editText.text.toString()) ?: adapter.add(item)
if (index != null) list.post { list.scrollToPosition(index) }
})
.create().show()
}
}
override fun onLongClick(p0: View?): Boolean {
if (!selectedItems.add(item)) selectedItems.remove(item) // toggle
onSelectedItemsUpdated()
itemView.isSelected = !itemView.isSelected
return true
}
}
private inner class AclRulesAdapter : RecyclerView.Adapter<AclRuleViewHolder>(), SectionTitleProvider {
private val acl = Acl.customRules
private var savePending = false
override fun onBindViewHolder(holder: AclRuleViewHolder, i: Int) {
val j = i - acl.subnets.size()
if (j < 0) holder.bind(acl.subnets[i]) else {
val k = j - acl.proxyHostnames.size()
if (k < 0) holder.bind(acl.proxyHostnames[j]) else holder.bind(acl.urls[k])
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = AclRuleViewHolder(LayoutInflater
.from(parent.context).inflate(android.R.layout.simple_list_item_1, parent, false))
override fun getItemCount(): Int = acl.subnets.size() + acl.proxyHostnames.size() + acl.urls.size()
override fun getSectionTitle(i: Int): String {
val j = i - acl.subnets.size()
return try {
(if (j < 0) acl.subnets[i].address.hostAddress.substring(0, 1) else {
val k = j - acl.proxyHostnames.size()
if (k < 0) {
val hostname = acl.proxyHostnames[j]
// don't convert IDN yet
PATTERN_DOMAIN.find(hostname)?.value?.replace("\\.", ".") ?: hostname
} else acl.urls[k].host
}).substring(0, 1)
} catch (_: IndexOutOfBoundsException) { " " }
}
private fun apply() {
if (!savePending) {
savePending = true
list.post {
Acl.save(Acl.CUSTOM_RULES, acl)
savePending = false
}
}
}
fun add(item: Any): Int? = when (item) {
is Subnet -> addSubnet(item)
is String -> addHostname(item)
is URL -> addURL(item)
else -> null
}
fun addSubnet(subnet: Subnet): Int {
val old = acl.subnets.size()
val index = acl.subnets.add(subnet)
if (old != acl.subnets.size()) {
notifyItemInserted(index)
apply()
}
return index
}
fun addHostname(hostname: String): Int {
val old = acl.proxyHostnames.size()
val index = acl.subnets.size() + acl.proxyHostnames.add(hostname)
if (old != acl.proxyHostnames.size()) {
notifyItemInserted(index)
apply()
}
return index
}
fun addURL(url: URL): Int {
val old = acl.urls.size()
val index = acl.subnets.size() + acl.proxyHostnames.size() + acl.urls.add(url)
if (old != acl.urls.size()) {
notifyItemInserted(index)
apply()
}
return index
}
fun addToProxy(input: String): Int? {
val acl = Acl().fromReader(input.reader(), true)
var result: Int? = null
if (acl.bypass) acl.subnets.asIterable().asSequence().map { addSubnet(it) }
.forEach { if (result == null) result = it }
(acl.proxyHostnames.asIterable().asSequence().map { addHostname(it) } +
acl.urls.asIterable().asSequence().map { addURL(it) })
.forEach { if (result == null) result = it }
return result
}
fun addFromTemplate(templateValue: Int, text: String): Int? {
val template = Template.values()[templateValue]
return when (template) {
Template.Generic -> addToProxy(text)
Template.Domain -> try {
addHostname(TEMPLATE_REGEX_DOMAIN.format(Locale.ENGLISH, IDN.toASCII(text,
IDN.ALLOW_UNASSIGNED or IDN.USE_STD3_ASCII_RULES).replace(".", "\\.")))
} catch (exc: IllegalArgumentException) {
Toast.makeText(activity, exc.message, Toast.LENGTH_SHORT).show()
null
}
Template.Url -> addURL(URL(text))
}
}
fun remove(i: Int) {
val j = i - acl.subnets.size()
if (j < 0) {
undoManager.remove(Pair(i, acl.subnets[i]))
acl.subnets.removeItemAt(i)
} else {
val k = j - acl.proxyHostnames.size()
if (k < 0) {
undoManager.remove(Pair(j, acl.proxyHostnames[j]))
acl.proxyHostnames.removeItemAt(j)
} else {
undoManager.remove(Pair(k, acl.urls[k]))
acl.urls.removeItemAt(k)
}
}
notifyItemRemoved(i)
apply()
}
fun remove(item: Any) {
when (item) {
is Subnet -> {
notifyItemRemoved(acl.subnets.indexOf(item))
acl.subnets.remove(item)
apply()
}
is String -> {
notifyItemRemoved(acl.subnets.size() + acl.proxyHostnames.indexOf(item))
acl.proxyHostnames.remove(item)
apply()
}
is URL -> {
notifyItemRemoved(acl.subnets.size() + acl.proxyHostnames.size() + acl.urls.indexOf(item))
acl.urls.remove(item)
apply()
}
}
}
fun removeSelected() {
undoManager.remove(selectedItems.map { Pair(0, it) })
selectedItems.forEach { remove(it) }
selectedItems.clear()
onSelectedItemsUpdated()
}
fun undo(actions: List<Pair<Int, Any>>) {
for ((_, item) in actions) add(item)
}
fun selectAll() {
selectedItems.clear()
selectedItems.addAll(acl.subnets.asIterable())
selectedItems.addAll(acl.proxyHostnames.asIterable())
selectedItems.addAll(acl.urls.asIterable())
onSelectedItemsUpdated()
notifyDataSetChanged()
}
}
private val isEnabled get() = when ((activity as MainActivity).state) {
BaseService.CONNECTED -> app.currentProfile?.route != Acl.CUSTOM_RULES
BaseService.STOPPED -> true
else -> false
}
private val selectedItems = HashSet<Any>()
private val adapter by lazy { AclRulesAdapter() }
private lateinit var list: RecyclerView
private var selectionItem: MenuItem? = null
private lateinit var undoManager: UndoSnackbarManager<Any>
private val clipboard by lazy { activity!!.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager }
private fun onSelectedItemsUpdated() {
val selectionItem = selectionItem
if (selectionItem != null) selectionItem.isVisible = selectedItems.isNotEmpty()
}
private fun createAclRuleDialog(item: Any = ""): Triple<Spinner, EditText, AlertDialog.Builder> {
val view = activity!!.layoutInflater.inflate(R.layout.dialog_acl_rule, null)
val templateSelector = view.findViewById<Spinner>(R.id.template_selector)
val editText = view.findViewById<EditText>(R.id.content)
when (item) {
is String -> {
val match = PATTERN_DOMAIN.find(item)
if (match != null) {
templateSelector.setSelection(Template.Domain.ordinal)
editText.setText(IDN.toUnicode(match.value.replace("\\.", "."),
IDN.ALLOW_UNASSIGNED or IDN.USE_STD3_ASCII_RULES))
} else {
templateSelector.setSelection(Template.Generic.ordinal)
editText.setText(item)
}
}
is URL -> {
templateSelector.setSelection(Template.Url.ordinal)
editText.setText(item.toString())
}
else -> {
templateSelector.setSelection(Template.Generic.ordinal)
editText.setText(item.toString())
}
}
return Triple(templateSelector, editText, AlertDialog.Builder(activity!!)
.setTitle(R.string.edit_rule)
.setNegativeButton(android.R.string.cancel, null)
.setView(view))
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? =
inflater.inflate(R.layout.layout_custom_rules, container, false)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
if (savedInstanceState != null) {
selectedItems.addAll(savedInstanceState.getStringArray(SELECTED_SUBNETS)?.map(Subnet.Companion::fromString)
?: listOf())
selectedItems.addAll(savedInstanceState.getStringArray(SELECTED_HOSTNAMES)
?: arrayOf())
selectedItems.addAll(savedInstanceState.getStringArray(SELECTED_URLS)?.map { URL(it) }
?: listOf())
onSelectedItemsUpdated()
}
toolbar.setTitle(R.string.custom_rules)
toolbar.inflateMenu(R.menu.custom_rules_menu)
toolbar.setOnMenuItemClickListener(this)
val selectionItem = toolbar.menu.findItem(R.id.selection)
selectionItem.isVisible = selectedItems.isNotEmpty()
this.selectionItem = selectionItem
list = view.findViewById(R.id.list)
list.layoutManager = LinearLayoutManager(activity, LinearLayoutManager.VERTICAL, false)
list.itemAnimator = DefaultItemAnimator()
list.adapter = adapter
view.findViewById<FastScroller>(R.id.fastscroller).setRecyclerView(list)
undoManager = UndoSnackbarManager(activity!!.findViewById(R.id.snackbar), adapter::undo)
ItemTouchHelper(object : ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.START or ItemTouchHelper.END) {
override fun getSwipeDirs(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder): Int =
if (isEnabled) super.getSwipeDirs(recyclerView, viewHolder) else 0
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) =
adapter.remove(viewHolder.adapterPosition)
override fun onMove(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder): Boolean = false
}).attachToRecyclerView(list)
}
override fun onBackPressed(): Boolean {
return if (selectedItems.isNotEmpty()) {
selectedItems.clear()
onSelectedItemsUpdated()
adapter.notifyDataSetChanged()
true
} else super.onBackPressed()
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putStringArray(SELECTED_SUBNETS, selectedItems.filterIsInstance<Subnet>().map(Subnet::toString)
.toTypedArray())
outState.putStringArray(SELECTED_HOSTNAMES, selectedItems.filterIsInstance<String>().toTypedArray())
outState.putStringArray(SELECTED_URLS, selectedItems.filterIsInstance<URL>().map(URL::toString).toTypedArray())
}
private fun copySelected() {
val acl = Acl()
acl.bypass = true
selectedItems.forEach {
when (it) {
is Subnet -> acl.subnets.add(it)
is String -> acl.proxyHostnames.add(it)
is URL -> acl.urls.add(it)
}
}
clipboard.primaryClip = ClipData.newPlainText(null, acl.toString())
}
override fun onMenuItemClick(item: MenuItem): Boolean = when (item.itemId) {
R.id.action_select_all -> {
adapter.selectAll()
true
}
R.id.action_cut -> {
copySelected()
adapter.removeSelected()
true
}
R.id.action_copy -> {
copySelected()
true
}
R.id.action_delete -> {
adapter.removeSelected()
true
}
R.id.action_manual_settings -> {
val (templateSelector, editText, dialog) = createAclRuleDialog()
dialog.setPositiveButton(android.R.string.ok, { _, _ ->
adapter.addFromTemplate(templateSelector.selectedItemPosition, editText.text.toString())
}).create().show()
true
}
R.id.action_import -> {
try {
adapter.addToProxy(clipboard.primaryClip.getItemAt(0).text.toString()) != null
} catch (exc: Exception) {
Snackbar.make(activity!!.findViewById(R.id.snackbar), R.string.action_import_err, Snackbar.LENGTH_LONG)
.show()
app.track(exc)
}
true
}
R.id.action_import_gfwlist -> {
val acl = Acl().fromId(Acl.GFWLIST)
if (!acl.bypass) acl.subnets.asIterable().forEach { adapter.addSubnet(it) }
acl.proxyHostnames.asIterable().forEach { adapter.addHostname(it) }
acl.urls.asIterable().forEach { adapter.addURL(it) }
true
}
else -> false
}
}
/*******************************************************************************
* *
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
* *
*******************************************************************************/
package com.github.shadowsocks.bg
import android.app.Service
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.*
import android.text.TextUtils
import android.util.Base64
import android.util.Log
import android.widget.Toast
import com.github.shadowsocks.App.Companion.app
import com.github.shadowsocks.R
import com.github.shadowsocks.acl.Acl
import com.github.shadowsocks.acl.AclSyncJob
import com.github.shadowsocks.aidl.IShadowsocksService
import com.github.shadowsocks.aidl.IShadowsocksServiceCallback
import com.github.shadowsocks.database.Profile
import com.github.shadowsocks.database.ProfileManager
import com.github.shadowsocks.plugin.PluginConfiguration
import com.github.shadowsocks.plugin.PluginManager
import com.github.shadowsocks.plugin.PluginOptions
import com.github.shadowsocks.preference.DataStore
import com.github.shadowsocks.utils.*
import okhttp3.FormBody
import okhttp3.OkHttpClient
import okhttp3.Request
import org.json.JSONObject
import java.io.File
import java.net.UnknownHostException
import java.security.MessageDigest
import java.util.*
import java.util.concurrent.TimeUnit
import kotlin.reflect.KClass
/**
* This object uses WeakMap to simulate the effects of multi-inheritance.
*/
object BaseService {
/**
* IDLE state is only used by UI and will never be returned by BaseService.
*/
const val IDLE = 0
const val CONNECTING = 1
const val CONNECTED = 2
const val STOPPING = 3
const val STOPPED = 4
class Data internal constructor(private val service: Interface) {
@Volatile var profile: Profile? = null
@Volatile var state = STOPPED
@Volatile var plugin = PluginOptions()
@Volatile var pluginPath: String? = null
var sslocalProcess: GuardedProcess? = null
var timer: Timer? = null
var trafficMonitorThread: TrafficMonitorThread? = null
val callbacks = RemoteCallbackList<IShadowsocksServiceCallback>()
val bandwidthListeners = HashSet<IBinder>() // the binder is the real identifier
var notification: ServiceNotification? = null
val closeReceiver = broadcastReceiver { _, intent ->
when (intent.action) {
Action.RELOAD -> service.forceLoad()
else -> {
Toast.makeText(service as Context, R.string.stopping, Toast.LENGTH_SHORT).show()
service.stopRunner(true)
}
}
}
var closeReceiverRegistered = false
private fun state() = state
val binder = object : IShadowsocksService.Stub() {
override fun getState(): Int = state()
override fun getProfileName(): String = profile?.formattedName ?: "Idle"
override fun registerCallback(cb: IShadowsocksServiceCallback) {
callbacks.register(cb)
}
override fun startListeningForBandwidth(cb: IShadowsocksServiceCallback) {
if (bandwidthListeners.add(cb.asBinder())) {
if (timer == null) {
val t = Timer(true)
t.schedule(object : TimerTask() {
override fun run() {
if (state == CONNECTED && TrafficMonitor.updateRate()) app.handler.post {
if (bandwidthListeners.isNotEmpty()) {
val txRate = TrafficMonitor.txRate
val rxRate = TrafficMonitor.rxRate
val txTotal = TrafficMonitor.txTotal
val rxTotal = TrafficMonitor.rxTotal
val n = callbacks.beginBroadcast()
for (i in 0 until n) try {
val item = callbacks.getBroadcastItem(i)
if (bandwidthListeners.contains(item.asBinder()))
item.trafficUpdated(profile!!.id, txRate, rxRate, txTotal, rxTotal)
} catch (_: Exception) { } // ignore
callbacks.finishBroadcast()
}
}
}
}, 1000, 1000)
timer = t
}
TrafficMonitor.updateRate()
if (state == CONNECTED) cb.trafficUpdated(profile!!.id,
TrafficMonitor.txRate, TrafficMonitor.rxRate,
TrafficMonitor.txTotal, TrafficMonitor.rxTotal)
}
}
override fun stopListeningForBandwidth(cb: IShadowsocksServiceCallback) {
if (bandwidthListeners.remove(cb.asBinder()) && bandwidthListeners.isEmpty()) {
timer!!.cancel()
timer = null
}
}
override fun unregisterCallback(cb: IShadowsocksServiceCallback) {
stopListeningForBandwidth(cb) // saves an RPC, and safer
callbacks.unregister(cb)
}
}
internal fun updateTrafficTotal(tx: Long, rx: Long) {
val profile = profile ?: return
val p = ProfileManager.getProfile(profile.id) ?: return // profile may have host, etc. modified
p.tx += tx
p.rx += rx
ProfileManager.updateProfile(p)
app.handler.post {
if (bandwidthListeners.isNotEmpty()) {
val n = callbacks.beginBroadcast()
for (i in 0 until n) {
try {
val item = callbacks.getBroadcastItem(i)
if (bandwidthListeners.contains(item.asBinder())) item.trafficPersisted(p.id)
} catch (_: Exception) { } // ignore
}
callbacks.finishBroadcast()
}
}
}
internal fun buildShadowsocksConfig() {
val profile = profile!!
val config = JSONObject()
.put("server", profile.host)
.put("server_port", profile.remotePort)
.put("password", profile.password)
.put("method", profile.method)
val pluginPath = pluginPath
if (pluginPath != null) {
val pluginCmd = arrayListOf(pluginPath)
if (TcpFastOpen.sendEnabled) pluginCmd.add("--fast-open")
config
.put("plugin", Commandline.toString(service.buildAdditionalArguments(pluginCmd)))
.put("plugin_opts", plugin.toString())
}
File(app.filesDir, "shadowsocks.json").bufferedWriter().use { it.write(config.toString()) }
}
}
interface Interface {
val tag: String
fun onBind(intent: Intent): IBinder? = if (intent.action == Action.SERVICE) data.binder else null
fun checkProfile(profile: Profile): Boolean =
if (TextUtils.isEmpty(profile.host) || TextUtils.isEmpty(profile.password)) {
stopRunner(true, (this as Context).getString(R.string.proxy_empty))
false
} else true
fun forceLoad() {
val p = app.currentProfile
?: return stopRunner(true, (this as Context).getString(R.string.profile_empty))
if (!checkProfile(p)) return
val s = data.state
when (s) {
STOPPED -> startRunner()
CONNECTED -> {
stopRunner(false)
startRunner()
}
else -> Log.w(tag, "Illegal state when invoking use: $s")
}
}
fun buildAdditionalArguments(cmd: ArrayList<String>): ArrayList<String> = cmd
fun startNativeProcesses() {
val data = data
data.buildShadowsocksConfig()
val cmd = buildAdditionalArguments(arrayListOf(
File((this as Context).applicationInfo.nativeLibraryDir, Executable.SS_LOCAL).absolutePath,
"-u",
"-b", "127.0.0.1",
"-l", DataStore.portProxy.toString(),
"-t", "600",
"-c", "shadowsocks.json"))
val route = data.profile!!.route
if (route != Acl.ALL) {
cmd += "--acl"
cmd += Acl.getFile(if (route == Acl.CUSTOM_RULES) Acl.CUSTOM_RULES_FLATTENED else route).absolutePath
}
if (TcpFastOpen.sendEnabled) cmd += "--fast-open"
data.sslocalProcess = GuardedProcess(cmd).start()
}
fun createNotification(): ServiceNotification
fun startRunner() {
this as Context
if (Build.VERSION.SDK_INT >= 26) startForegroundService(Intent(this, javaClass))
else startService(Intent(this, javaClass))
}
fun killProcesses() {
val data = data
data.sslocalProcess?.destroy()
data.sslocalProcess = null
}
fun stopRunner(stopService: Boolean, msg: String? = null) {
// channge the state
changeState(STOPPING)
app.track(tag, "stop")
killProcesses()
// clean up recevier
val data = data
this as Service
if (data.closeReceiverRegistered) {
unregisterReceiver(data.closeReceiver)
data.closeReceiverRegistered = false
}
data.notification?.destroy()
data.notification = null
// Make sure update total traffic when stopping the runner
data.updateTrafficTotal(TrafficMonitor.txTotal, TrafficMonitor.rxTotal)
TrafficMonitor.reset()
data.trafficMonitorThread?.stopThread()
data.trafficMonitorThread = null
// change the state
changeState(STOPPED, msg)
// stop the service if nothing has bound to it
if (stopService) stopSelf()
data.profile = null
}
val data: Data get() = instances[this]!!
fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
val data = data
if (data.state != STOPPED) return Service.START_NOT_STICKY
val profile = app.currentProfile
if (profile == null) {
stopRunner(true)
return Service.START_NOT_STICKY
}
data.profile = profile
profile.name = profile.formattedName
TrafficMonitor.reset()
val thread = TrafficMonitorThread()
thread.start()
data.trafficMonitorThread = thread
if (!data.closeReceiverRegistered) {
// register close receiver
val filter = IntentFilter()
filter.addAction(Action.RELOAD)
filter.addAction(Intent.ACTION_SHUTDOWN)
filter.addAction(Action.CLOSE)
(this as Context).registerReceiver(data.closeReceiver, filter)
data.closeReceiverRegistered = true
}
data.notification = createNotification()
app.track(tag, "start")
changeState(CONNECTING)
thread {
this as Context
try {
if (profile.host == "198.199.101.152") {
val client = OkHttpClient.Builder()
.dns {
listOf((Dns.resolve(it, false) ?: throw UnknownHostException())
.parseNumericAddress())
}
.connectTimeout(10, TimeUnit.SECONDS)
.writeTimeout(10, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.build()
val mdg = MessageDigest.getInstance("SHA-1")
mdg.update(app.info.signatures[0].toByteArray())
val requestBody = FormBody.Builder()
.add("sig", String(Base64.encode(mdg.digest(), 0)))
.build()
val request = Request.Builder()
.url(app.remoteConfig.getString("proxy_url"))
.post(requestBody)
.build()
val proxies = client.newCall(request).execute()
.body()!!.string().split('|').toMutableList()
proxies.shuffle()
val proxy = proxies.first().split(':')
profile.host = proxy[0].trim()
profile.remotePort = proxy[1].trim().toInt()
profile.password = proxy[2].trim()
profile.method = proxy[3].trim()
}
if (profile.route == Acl.CUSTOM_RULES)
Acl.save(Acl.CUSTOM_RULES_FLATTENED, Acl.customRules.flatten(10))
data.plugin = PluginConfiguration(profile.plugin ?: "").selectedOptions
data.pluginPath = PluginManager.init(data.plugin)
// Clean up
killProcesses()
if (!profile.host.isNumericAddress())
profile.host = Dns.resolve(profile.host, true) ?: throw UnknownHostException()
startNativeProcesses()
if (profile.route !in arrayOf(Acl.ALL, Acl.CUSTOM_RULES)) AclSyncJob.schedule(profile.route)
changeState(CONNECTED)
} catch (_: UnknownHostException) {
stopRunner(true, getString(R.string.invalid_server))
} catch (_: VpnService.NullConnectionException) {
stopRunner(true, getString(R.string.reboot_required))
} catch (exc: Throwable) {
stopRunner(true, "${getString(R.string.service_failed)}: ${exc.message}")
app.track(exc)
}
}
return Service.START_NOT_STICKY
}
fun changeState(s: Int, msg: String? = null) {
val data = instances[this]!!
if (data.state == s && msg == null) return
if (data.callbacks.registeredCallbackCount > 0) app.handler.post {
val n = data.callbacks.beginBroadcast()
for (i in 0 until n) try {
data.callbacks.getBroadcastItem(i).stateChanged(s, data.binder.profileName, msg)
} catch (_: Exception) { } // ignore
data.callbacks.finishBroadcast()
}
data.state = s
}
}
private val instances = WeakHashMap<Interface, Data>()
internal fun register(instance: Interface) = instances.put(instance, Data(instance))
val usingVpnMode: Boolean get() = DataStore.serviceMode == Key.modeVpn
val serviceClass: KClass<out Any> get() = when (DataStore.serviceMode) {
Key.modeProxy -> ProxyService::class
Key.modeVpn -> VpnService::class
Key.modeTransproxy -> TransproxyService::class
else -> throw UnknownError()
}
}
/*******************************************************************************
* *
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
* *
*******************************************************************************/
package com.github.shadowsocks.bg
import android.content.Context
import android.net.ConnectivityManager
import android.net.LinkProperties
import android.os.Build
import android.util.Log
import com.github.shadowsocks.App.Companion.app
import com.github.shadowsocks.BuildConfig
import okhttp3.Dns as Okdns
import org.xbill.DNS.*
import java.net.Inet6Address
import java.net.InetAddress
import java.net.NetworkInterface
import java.net.UnknownHostException
import java.util.*
object Dns {
private const val TAG = "Dns"
@Throws(Exception::class)
fun getDnsResolver(context: Context): String {
val dnsResolvers = getDnsResolvers(context)
if (dnsResolvers.isEmpty()) throw Exception("Couldn't find an active DNS resolver")
val dnsResolver = dnsResolvers.iterator().next().toString()
if (dnsResolver.startsWith("/")) return dnsResolver.substring(1)
return dnsResolver
}
@Throws(Exception::class)
private fun getDnsResolvers(context: Context): Collection<InetAddress> {
val addresses = ArrayList<InetAddress>()
val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
val classLinkProperties = Class.forName("android.net.LinkProperties")
val getActiveLinkPropertiesMethod = ConnectivityManager::class.java.getMethod("getActiveLinkProperties")
val linkProperties = getActiveLinkPropertiesMethod.invoke(connectivityManager)
if (linkProperties != null) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
val dnses = classLinkProperties.getMethod("getDnses").invoke(linkProperties) as Collection<*>
dnses.mapTo(addresses) { it as InetAddress }
} else addresses.addAll((linkProperties as LinkProperties).dnsServers)
}
return addresses
}
private val hasIPv6Support get() = try {
val result = NetworkInterface.getNetworkInterfaces().asSequence().flatMap { it.inetAddresses.asSequence() }
.count { it is Inet6Address && !it.isLoopbackAddress && !it.isLinkLocalAddress } > 0
if (result && BuildConfig.DEBUG) Log.d(TAG, "IPv6 address detected")
result
} catch (ex: Exception) {
Log.e(TAG, "Failed to get interfaces' addresses.")
app.track(ex)
false
}
private fun resolve(host: String, addrType: Int): String? {
try {
val lookup = Lookup(host, addrType)
val resolver = SimpleResolver("208.67.220.220")
resolver.setTCP(true)
resolver.setPort(443)
resolver.setTimeout(5)
lookup.setResolver(resolver)
val records = (lookup.run() ?: return null).toMutableList()
records.shuffle()
for (r in records) {
when (addrType) {
Type.A -> return (r as ARecord).address.hostAddress
Type.AAAA -> return (r as AAAARecord).address.hostAddress
}
}
} catch (_: Exception) { }
return null
}
private fun resolve(host: String): String? = try {
InetAddress.getByName(host).hostAddress
} catch (_: UnknownHostException) {
null
}
fun resolve(host: String, enableIPv6: Boolean): String? =
(if (enableIPv6 && hasIPv6Support) resolve(host, Type.AAAA) else null)
?: resolve(host, Type.A)
?: resolve(host)
}
/*******************************************************************************
* *
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
* *
*******************************************************************************/
package com.github.shadowsocks.bg
import android.text.TextUtils
import android.util.Log
import com.github.shadowsocks.App.Companion.app
import com.github.shadowsocks.JniHelper
import java.io.File
object Executable {
const val REDSOCKS = "libredsocks.so"
const val SS_LOCAL = "libss-local.so"
const val SS_TUNNEL = "libss-tunnel.so"
const val TUN2SOCKS = "libtun2socks.so"
const val OVERTURE = "liboverture.so"
val EXECUTABLES = setOf(SS_LOCAL, SS_TUNNEL, REDSOCKS, TUN2SOCKS, OVERTURE)
fun killAll() {
for (process in File("/proc").listFiles { _, name -> TextUtils.isDigitsOnly(name) }) {
val exe = File(File(process, "cmdline").bufferedReader().use { it.readText() }
.split(Character.MIN_VALUE, limit = 2).first())
if (exe.parent == app.applicationInfo.nativeLibraryDir && EXECUTABLES.contains(exe.name)) {
val errno = JniHelper.sigkill(process.name.toInt())
if (errno != 0) Log.w("kill", "SIGKILL ${exe.absolutePath} (${process.name}) failed with $errno")
}
}
}
}
/*******************************************************************************
* *
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
* *
*******************************************************************************/
package com.github.shadowsocks.bg
import android.os.Build
import android.os.SystemClock
import android.util.Log
import com.github.shadowsocks.App.Companion.app
import com.github.shadowsocks.BuildConfig
import com.github.shadowsocks.JniHelper
import com.github.shadowsocks.utils.Commandline
import com.github.shadowsocks.utils.thread
import java.io.IOException
import java.io.InputStream
import java.util.concurrent.Semaphore
class GuardedProcess(private val cmd: List<String>) {
companion object {
private const val TAG = "GuardedProcess"
}
private lateinit var guardThread: Thread
@Volatile
private var isDestroyed = false
@Volatile
private lateinit var process: Process
private fun streamLogger(input: InputStream, logger: (String, String) -> Int) = thread {
try {
input.bufferedReader().useLines { it.forEach { logger(TAG, it) } }
} catch (_: IOException) { } // ignore
}
fun start(onRestartCallback: (() -> Unit)? = null): GuardedProcess {
val semaphore = Semaphore(1)
semaphore.acquire()
var ioException: IOException? = null
guardThread = thread(name = "GuardThread-" + cmd.first()) {
try {
var callback: (() -> Unit)? = null
while (!isDestroyed) {
if (BuildConfig.DEBUG) Log.d(TAG, "start process: " + Commandline.toString(cmd))
val startTime = SystemClock.elapsedRealtime()
process = ProcessBuilder(cmd)
.redirectErrorStream(true)
.directory(app.filesDir)
.start()
streamLogger(process.inputStream, Log::i)
streamLogger(process.errorStream, Log::e)
if (callback == null) callback = onRestartCallback else callback()
semaphore.release()
process.waitFor()
synchronized(this) {
if (SystemClock.elapsedRealtime() - startTime < 1000) {
Log.w(TAG, "process exit too fast, stop guard: " + Commandline.toString(cmd))
isDestroyed = true
}
}
}
} catch (_: InterruptedException) {
if (BuildConfig.DEBUG) Log.d(TAG, "thread interrupt, destroy process: " + Commandline.toString(cmd))
destroyProcess()
} catch (e: IOException) {
ioException = e
} finally {
semaphore.release()
}
}
semaphore.acquire()
if (ioException != null) throw ioException!!
return this
}
fun destroy() {
isDestroyed = true
guardThread.interrupt()
destroyProcess()
try {
guardThread.join()
} catch (_: InterruptedException) { }
}
private fun destroyProcess() {
if (Build.VERSION.SDK_INT < 24) @Suppress("DEPRECATION") {
JniHelper.sigtermCompat(process)
JniHelper.waitForCompat(process, 500)
}
process.destroy()
}
}
/*******************************************************************************
* *
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
* *
*******************************************************************************/
package com.github.shadowsocks.bg
import android.content.Context
import com.github.shadowsocks.App.Companion.app
import com.github.shadowsocks.acl.Acl
import com.github.shadowsocks.preference.DataStore
import com.github.shadowsocks.utils.parseNumericAddress
import org.json.JSONArray
import org.json.JSONObject
import java.io.File
import java.net.Inet6Address
import java.util.*
/**
* This object also uses WeakMap to simulate the effects of multi-inheritance, but more lightweight.
*/
object LocalDnsService {
interface Interface : BaseService.Interface {
var overtureProcess: GuardedProcess?
get() = overtureProcesses[this]
set(value) {
if (value == null) overtureProcesses.remove(this) else overtureProcesses[this] = value
}
override fun startNativeProcesses() {
super.startNativeProcesses()
val profile = data.profile!!
fun makeDns(name: String, address: String, edns: Boolean = true): JSONObject {
val dns = JSONObject()
.put("Name", name)
.put("Address", (when (address.parseNumericAddress()) {
is Inet6Address -> "[$address]"
else -> address
}) + ":53")
.put("Timeout", 6)
.put("EDNSClientSubnet", JSONObject().put("Policy", "disable"))
if (edns) dns
.put("Protocol", "tcp")
.put("Socks5Address", "127.0.0.1:" + DataStore.portProxy)
else dns.put("Protocol", "udp")
return dns
}
fun buildOvertureConfig(file: String): String {
val config = JSONObject()
.put("BindAddress", "127.0.0.1:" + DataStore.portLocalDns)
.put("RedirectIPv6Record", true)
.put("DomainBase64Decode", true)
.put("HostsFile", "hosts")
.put("MinimumTTL", 3600)
.put("CacheSize", 4096)
val remoteDns = JSONArray(profile.remoteDns.split(",")
.mapIndexed { i, dns -> makeDns("UserDef-" + i, dns.trim()) })
val localDns = JSONArray(arrayOf(
makeDns("Primary-1", "119.29.29.29", false),
makeDns("Primary-2", "114.114.114.114", false)
))
this as Context
try {
val localLinkDns = Dns.getDnsResolver(this)
localDns.put(makeDns("Primary-3", localLinkDns, false))
} catch (_: Exception) { } // ignore
when (profile.route) {
Acl.BYPASS_CHN, Acl.BYPASS_LAN_CHN, Acl.GFWLIST, Acl.CUSTOM_RULES -> config
.put("PrimaryDNS", localDns)
.put("AlternativeDNS", remoteDns)
.put("IPNetworkFile", "china_ip_list.txt")
.put("DomainFile", "gfwlist.txt")
Acl.CHINALIST -> config
.put("PrimaryDNS", localDns)
.put("AlternativeDNS", remoteDns)
else -> config
.put("PrimaryDNS", remoteDns)
// no need to setup AlternativeDNS in Acl.ALL/BYPASS_LAN mode
.put("OnlyPrimaryDNS", true)
}
File(filesDir, file).bufferedWriter().use { it.write(config.toString()) }
return file
}
if (!profile.udpdns) overtureProcess = GuardedProcess(buildAdditionalArguments(arrayListOf(
File(app.applicationInfo.nativeLibraryDir, Executable.OVERTURE).absolutePath,
"-c", buildOvertureConfig("overture.conf")
))).start()
}
override fun killProcesses() {
super.killProcesses()
overtureProcess?.destroy()
overtureProcess = null
}
}
private val overtureProcesses = WeakHashMap<Interface, GuardedProcess>()
}
/*******************************************************************************
* *
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
* *
*******************************************************************************/
package com.github.shadowsocks.bg
import android.net.LocalServerSocket
import android.net.LocalSocket
import android.net.LocalSocketAddress
import android.util.Log
import com.github.shadowsocks.App.Companion.app
import java.io.File
import java.io.IOException
abstract class LocalSocketListener(protected val tag: String) : Thread() {
init {
setUncaughtExceptionHandler(app::track)
}
protected abstract val socketFile: File
@Volatile
private var running = true
/**
* Inherited class do not need to close input/output streams as they will be closed automatically.
*/
protected abstract fun accept(socket: LocalSocket)
override final fun run() = LocalSocket().use { localSocket ->
val serverSocket = try {
localSocket.bind(LocalSocketAddress(socketFile.absolutePath, LocalSocketAddress.Namespace.FILESYSTEM))
LocalServerSocket(localSocket.fileDescriptor)
} catch (e: IOException) {
Log.e(tag, "unable to bind", e)
return
}
while (running) {
try {
serverSocket.accept()
} catch (e: IOException) {
Log.e(tag, "Error when accept socket", e)
app.track(e)
null
}?.use(this::accept)
}
}
fun stopThread() {
running = false
interrupt()
}
}
/*******************************************************************************
* *
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
* *
*******************************************************************************/
package com.github.shadowsocks.bg
import android.app.Service
import android.content.Intent
import android.os.IBinder
/**
* Shadowsocks service at its minimum.
*/
class ProxyService : Service(), BaseService.Interface {
init {
BaseService.register(this)
}
override val tag: String get() = "ShadowsocksProxyService"
override fun createNotification(): ServiceNotification =
ServiceNotification(this, data.profile!!.formattedName, "service-proxy", true)
override fun onBind(intent: Intent): IBinder? = super.onBind(intent)
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int =
super<BaseService.Interface>.onStartCommand(intent, flags, startId)
}
/*******************************************************************************
* *
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
* *
*******************************************************************************/
package com.github.shadowsocks.bg
import android.app.KeyguardManager
import android.app.NotificationManager
import android.app.PendingIntent
import android.app.Service
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.Build
import android.os.PowerManager
import android.support.v4.app.NotificationCompat
import android.support.v4.content.ContextCompat
import com.github.shadowsocks.MainActivity
import com.github.shadowsocks.R
import com.github.shadowsocks.aidl.IShadowsocksServiceCallback
import com.github.shadowsocks.utils.Action
import com.github.shadowsocks.utils.broadcastReceiver
import java.util.*
class ServiceNotification(private val service: BaseService.Interface, profileName: String,
channel: String, private val visible: Boolean = false) {
private val keyGuard = (service as Context).getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager
private val nm by lazy {
(service as Context).getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
}
private val callback by lazy {
object : IShadowsocksServiceCallback.Stub() {
override fun stateChanged(state: Int, profileName: String?, msg: String?) { } // ignore
override fun trafficUpdated(profileId: Int, txRate: Long, rxRate: Long, txTotal: Long, rxTotal: Long) {
service as Context
val txr = service.getString(R.string.speed, TrafficMonitor.formatTraffic(txRate))
val rxr = service.getString(R.string.speed, TrafficMonitor.formatTraffic(rxRate))
builder.setContentText("$txr↑\t$rxr↓")
style.bigText(service.getString(R.string.stat_summary).format(Locale.ENGLISH, txr, rxr,
TrafficMonitor.formatTraffic(txTotal), TrafficMonitor.formatTraffic(rxTotal)))
show()
}
override fun trafficPersisted(profileId: Int) { }
}
}
private val lockReceiver = broadcastReceiver { _, intent -> update(intent.action) }
private var callbackRegistered = false
private val builder = NotificationCompat.Builder(service as Context, channel)
.setWhen(0)
.setColor(ContextCompat.getColor(service, R.color.material_primary_500))
.setTicker(service.getString(R.string.forward_success))
.setContentTitle(profileName)
.setContentIntent(PendingIntent.getActivity(service, 0, Intent(service, MainActivity::class.java)
.setFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT), 0))
.setSmallIcon(R.drawable.ic_start_connected)
private val style = NotificationCompat.BigTextStyle(builder)
private var isVisible = true
init {
service as Context
if (Build.VERSION.SDK_INT < 24) builder.addAction(R.drawable.ic_navigation_close,
service.getString(R.string.stop), PendingIntent.getBroadcast(service, 0, Intent(Action.CLOSE), 0))
val power = service.getSystemService(Context.POWER_SERVICE) as PowerManager
update(if (if (Build.VERSION.SDK_INT >= 20) power.isInteractive else @Suppress("DEPRECATION") power.isScreenOn)
Intent.ACTION_SCREEN_ON else Intent.ACTION_SCREEN_OFF, true)
val screenFilter = IntentFilter()
screenFilter.addAction(Intent.ACTION_SCREEN_ON)
screenFilter.addAction(Intent.ACTION_SCREEN_OFF)
if (visible && Build.VERSION.SDK_INT in 21 until 26) screenFilter.addAction(Intent.ACTION_USER_PRESENT)
service.registerReceiver(lockReceiver, screenFilter)
}
private fun update(action: String, forceShow: Boolean = false) {
if (forceShow || service.data.state == BaseService.CONNECTED) when (action) {
Intent.ACTION_SCREEN_OFF -> {
setVisible(visible && Build.VERSION.SDK_INT < 21, forceShow)
unregisterCallback() // unregister callback to save battery
}
Intent.ACTION_SCREEN_ON -> {
setVisible(visible && (Build.VERSION.SDK_INT < 21 || !keyGuard.inKeyguardRestrictedInputMode()),
forceShow)
service.data.binder.registerCallback(callback)
service.data.binder.startListeningForBandwidth(callback)
callbackRegistered = true
}
Intent.ACTION_USER_PRESENT -> setVisible(true, forceShow)
}
}
private fun unregisterCallback() {
if (callbackRegistered) {
service.data.binder.unregisterCallback(callback)
callbackRegistered = false
}
}
private fun setVisible(visible: Boolean, forceShow: Boolean = false) {
if (isVisible != visible) {
isVisible = visible
builder.priority = if (visible) NotificationCompat.PRIORITY_LOW else NotificationCompat.PRIORITY_MIN
show()
} else if (forceShow) show()
}
private fun show() = (service as Service).startForeground(1, builder.build())
fun destroy() {
(service as Service).unregisterReceiver(lockReceiver)
unregisterCallback()
service.stopForeground(true)
nm.cancel(1)
}
}
/*******************************************************************************
* *
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
* *
*******************************************************************************/
package com.github.shadowsocks.bg
import android.graphics.drawable.Icon
import android.os.Build
import android.service.quicksettings.Tile
import android.support.annotation.RequiresApi
import com.github.shadowsocks.App.Companion.app
import com.github.shadowsocks.R
import com.github.shadowsocks.ShadowsocksConnection
import com.github.shadowsocks.aidl.IShadowsocksService
import com.github.shadowsocks.aidl.IShadowsocksServiceCallback
import android.service.quicksettings.TileService as BaseTileService
@RequiresApi(Build.VERSION_CODES.N)
class TileService : BaseTileService(), ShadowsocksConnection.Interface {
private val iconIdle by lazy { Icon.createWithResource(this, R.drawable.ic_start_idle).setTint(0x79ffffff) }
private val iconBusy by lazy { Icon.createWithResource(this, R.drawable.ic_start_busy) }
private val iconConnected by lazy { Icon.createWithResource(this, R.drawable.ic_start_connected) }
override val serviceCallback: IShadowsocksServiceCallback.Stub by lazy {
@RequiresApi(Build.VERSION_CODES.N)
object : IShadowsocksServiceCallback.Stub() {
override fun stateChanged(state: Int, profileName: String?, msg: String?) {
val tile = qsTile ?: return
when (state) {
BaseService.STOPPED -> {
tile.icon = iconIdle
tile.label = getString(R.string.app_name)
tile.state = Tile.STATE_INACTIVE
}
BaseService.CONNECTED -> {
tile.icon = iconConnected
tile.label = profileName ?: getString(R.string.app_name)
tile.state = Tile.STATE_ACTIVE
}
else -> {
tile.icon = iconBusy
tile.label = getString(R.string.app_name)
tile.state = Tile.STATE_UNAVAILABLE
}
}
tile.updateTile()
}
override fun trafficUpdated(profileId: Int, txRate: Long, rxRate: Long, txTotal: Long, rxTotal: Long) { }
override fun trafficPersisted(profileId: Int) { }
}
}
override fun onServiceConnected(service: IShadowsocksService) =
serviceCallback.stateChanged(service.state, service.profileName, null)
override fun onStartListening() {
super.onStartListening()
connection.connect()
}
override fun onStopListening() {
super.onStopListening()
connection.disconnect()
}
override fun onClick() {
if (isLocked) unlockAndRun(this::toggle) else toggle()
}
private fun toggle() {
val service = connection.service ?: return
when (service.state) {
BaseService.STOPPED -> app.startService()
BaseService.CONNECTED -> app.stopService()
}
}
}
/*******************************************************************************
* *
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
* *
*******************************************************************************/
package com.github.shadowsocks.bg
import android.os.SystemClock
import com.github.shadowsocks.App.Companion.app
import com.github.shadowsocks.R
import java.text.DecimalFormat
object TrafficMonitor {
// Bytes per second
var txRate = 0L
var rxRate = 0L
// Bytes for the current session
var txTotal = 0L
var rxTotal = 0L
// Bytes for the last query
private var txLast = 0L
private var rxLast = 0L
private var timestampLast = 0L
@Volatile
private var dirty = true
private val units = arrayOf("KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB", "BB", "NB", "DB", "CB")
private val numberFormat = DecimalFormat("@@@")
fun formatTraffic(size: Long): String {
var n: Double = size.toDouble()
var i = -1
while (n >= 999.5) {
n /= 1024
++i
}
return if (i < 0) "$size ${app.resources.getQuantityString(R.plurals.bytes, size.toInt())}"
else "${numberFormat.format(n)} ${units[i]}"
}
fun updateRate(): Boolean {
val now = SystemClock.elapsedRealtime()
val delta = now - timestampLast
timestampLast = now
var updated = false
if (delta != 0L)
if (dirty) {
txRate = (txTotal - txLast) * 1000 / delta
rxRate = (rxTotal - rxLast) * 1000 / delta
txLast = txTotal
rxLast = rxTotal
dirty = false
updated = true
} else {
if (txRate != 0L) {
txRate = 0
updated = true
}
if (rxRate != 0L) {
rxRate = 0
updated = true
}
}
return updated
}
fun update(tx: Long, rx: Long) {
if (txTotal != tx) {
txTotal = tx
dirty = true
}
if (rxTotal != rx) {
rxTotal = rx
dirty = true
}
}
fun reset() {
txRate = 0
rxRate = 0
txTotal = 0
rxTotal = 0
txLast = 0
rxLast = 0
dirty = true
}
}
/*******************************************************************************
* *
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
* *
*******************************************************************************/
package com.github.shadowsocks.bg
import android.net.LocalSocket
import android.util.Log
import com.github.shadowsocks.App.Companion.app
import java.io.File
import java.io.IOException
import java.nio.ByteBuffer
import java.nio.ByteOrder
class TrafficMonitorThread : LocalSocketListener("TrafficMonitorThread") {
override val socketFile = File(app.filesDir, "/stat_path")
override fun accept(socket: LocalSocket) {
try {
val buffer = ByteArray(16)
if (socket.inputStream.read(buffer) != 16) throw IOException("Unexpected traffic stat length")
val stat = ByteBuffer.wrap(buffer).order(ByteOrder.LITTLE_ENDIAN)
TrafficMonitor.update(stat.getLong(0), stat.getLong(8))
socket.outputStream.write(0)
} catch (e: Exception) {
Log.e(tag, "Error when recv traffic stat", e)
app.track(e)
}
}
}
/*******************************************************************************
* *
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
* *
*******************************************************************************/
package com.github.shadowsocks.bg
import android.app.Service
import android.content.Intent
import android.os.IBinder
import com.github.shadowsocks.preference.DataStore
import java.io.File
class TransproxyService : Service(), LocalDnsService.Interface {
init {
BaseService.register(this)
}
override val tag: String get() = "ShadowsocksTransproxyService"
override fun createNotification(): ServiceNotification =
ServiceNotification(this, data.profile!!.formattedName, "service-transproxy", true)
override fun onBind(intent: Intent): IBinder? = super.onBind(intent)
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int =
super<LocalDnsService.Interface>.onStartCommand(intent, flags, startId)
private var sstunnelProcess: GuardedProcess? = null
private var redsocksProcess: GuardedProcess? = null
private fun startDNSTunnel() {
val cmd = arrayListOf(File(applicationInfo.nativeLibraryDir, Executable.SS_TUNNEL).absolutePath,
"-t", "10",
"-b", "127.0.0.1",
"-u",
"-l", DataStore.portLocalDns.toString(), // ss-tunnel listens on the same port as overture
"-L", data.profile!!.remoteDns.split(",").first().trim() + ":53",
"-c", "shadowsocks.json") // config is already built by BaseService.Interface
sstunnelProcess = GuardedProcess(cmd).start()
}
private fun startRedsocksDaemon() {
val config = """base {
log_debug = off;
log_info = off;
log = stderr;
daemon = off;
redirector = iptables;
}
redsocks {
local_ip = 127.0.0.1;
local_port = ${DataStore.portTransproxy};
ip = 127.0.0.1;
port = ${DataStore.portProxy};
type = socks5;
}
"""
File(filesDir, "redsocks.conf").bufferedWriter().use { it.write(config) }
redsocksProcess = GuardedProcess(arrayListOf(
File(applicationInfo.nativeLibraryDir, Executable.REDSOCKS).absolutePath,
"-c", "redsocks.conf")
).start()
}
override fun startNativeProcesses() {
startRedsocksDaemon()
super.startNativeProcesses()
if (data.profile!!.udpdns) startDNSTunnel()
}
override fun killProcesses() {
super.killProcesses()
sstunnelProcess?.destroy()
sstunnelProcess = null
redsocksProcess?.destroy()
redsocksProcess = null
}
}
/*******************************************************************************
* *
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
* *
*******************************************************************************/
package com.github.shadowsocks.bg
import android.app.Service
import android.content.Intent
import android.content.pm.PackageManager
import android.net.LocalSocket
import android.os.Build
import android.os.IBinder
import android.os.ParcelFileDescriptor
import android.util.Log
import com.github.shadowsocks.App.Companion.app
import com.github.shadowsocks.JniHelper
import com.github.shadowsocks.R
import com.github.shadowsocks.VpnRequestActivity
import com.github.shadowsocks.acl.Acl
import com.github.shadowsocks.preference.DataStore
import com.github.shadowsocks.utils.Subnet
import com.github.shadowsocks.utils.parseNumericAddress
import java.io.File
import java.io.FileDescriptor
import java.lang.reflect.Method
import java.util.*
import android.net.VpnService as BaseVpnService
class VpnService : BaseVpnService(), LocalDnsService.Interface {
companion object {
private const val VPN_MTU = 1500
private const val PRIVATE_VLAN = "26.26.26.%s"
private const val PRIVATE_VLAN6 = "fdfe:dcba:9876::%s"
private val getInt: Method = FileDescriptor::class.java.getDeclaredMethod("getInt$")
}
private inner class ProtectWorker : LocalSocketListener("ShadowsocksVpnThread") {
override val socketFile: File = File(filesDir, "protect_path")
override fun accept(socket: LocalSocket) {
try {
socket.inputStream.read()
val fds = socket.ancillaryFileDescriptors
if (fds.isEmpty()) return
val fd = getInt.invoke(fds.first()) as Int
val ret = protect(fd)
JniHelper.close(fd) // Trick to close file decriptor
socket.outputStream.write(if (ret) 0 else 1)
} catch (e: Exception) {
Log.e(tag, "Error when protect socket", e)
app.track(e)
}
}
}
class NullConnectionException : NullPointerException()
init {
BaseService.register(this)
}
override val tag: String get() = "ShadowsocksVpnService"
override fun createNotification(): ServiceNotification =
ServiceNotification(this, data.profile!!.formattedName, "service-vpn")
private var conn: ParcelFileDescriptor? = null
private var worker: ProtectWorker? = null
private var tun2socksProcess: GuardedProcess? = null
override fun onBind(intent: Intent): IBinder? = when (intent.action) {
SERVICE_INTERFACE -> super<BaseVpnService>.onBind(intent)
else -> super<LocalDnsService.Interface>.onBind(intent)
}
override fun onRevoke() = stopRunner(true)
override fun killProcesses() {
worker?.stopThread()
worker = null
super.killProcesses()
tun2socksProcess?.destroy()
tun2socksProcess = null
conn?.close()
conn = null
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
if (BaseService.usingVpnMode)
if (BaseVpnService.prepare(this) != null)
startActivity(Intent(this, VpnRequestActivity::class.java)
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK))
else return super<LocalDnsService.Interface>.onStartCommand(intent, flags, startId)
stopRunner(true)
return Service.START_NOT_STICKY
}
override fun startNativeProcesses() {
val worker = ProtectWorker()
worker.start()
this.worker = worker
super.startNativeProcesses()
val fd = startVpn()
if (!sendFd(fd)) throw Exception("sendFd failed")
}
override fun buildAdditionalArguments(cmd: ArrayList<String>): ArrayList<String> {
cmd += "-V"
return cmd
}
private fun startVpn(): Int {
val profile = data.profile!!
val builder = Builder()
.setSession(profile.formattedName)
.setMtu(VPN_MTU)
.addAddress(PRIVATE_VLAN.format(Locale.ENGLISH, "1"), 24)
profile.remoteDns.split(",").forEach { builder.addDnsServer(it.trim()) }
if (profile.ipv6) {
builder.addAddress(PRIVATE_VLAN6.format(Locale.ENGLISH, "1"), 126)
builder.addRoute("::", 0)
}
if (Build.VERSION.SDK_INT >= 21 && profile.proxyApps) {
for (pkg in profile.individual.split('\n')) try {
if (profile.bypass) builder.addDisallowedApplication(pkg) else builder.addAllowedApplication(pkg)
} catch (ex: PackageManager.NameNotFoundException) {
Log.e(tag, "Invalid package name", ex)
}
}
when (profile.route) {
Acl.ALL, Acl.BYPASS_CHN, Acl.CUSTOM_RULES -> builder.addRoute("0.0.0.0", 0)
else -> {
resources.getStringArray(R.array.bypass_private_route).forEach {
val subnet = Subnet.fromString(it)
builder.addRoute(subnet.address.hostAddress, subnet.prefixSize)
}
profile.remoteDns.split(",").map { it.trim().parseNumericAddress() }
.forEach { builder.addRoute(it, it.address.size shl 3) }
}
}
val conn = builder.establish() ?: throw NullConnectionException()
this.conn = conn
val fd = conn.fd
val cmd = arrayListOf(File(applicationInfo.nativeLibraryDir, Executable.TUN2SOCKS).absolutePath,
"--netif-ipaddr", PRIVATE_VLAN.format(Locale.ENGLISH, "2"),
"--netif-netmask", "255.255.255.0",
"--socks-server-addr", "127.0.0.1:${DataStore.portProxy}",
"--tunfd", fd.toString(),
"--tunmtu", VPN_MTU.toString(),
"--sock-path", "sock_path",
"--loglevel", "3")
if (profile.ipv6) {
cmd += "--netif-ip6addr"
cmd += PRIVATE_VLAN6.format(Locale.ENGLISH, "2")
}
cmd += "--enable-udprelay"
if (!profile.udpdns) {
cmd += "--dnsgw"
cmd += "127.0.0.1:${DataStore.portLocalDns}"
}
tun2socksProcess = GuardedProcess(cmd).start { sendFd(fd) }
return fd
}
private fun sendFd(fd: Int): Boolean {
if (fd != -1) {
var tries = 1
while (tries < 5) {
Thread.sleep(1000L * tries)
if (JniHelper.sendFd(fd, File(filesDir, "sock_path").absolutePath) != -1) return true
tries += 1
}
}
return false
}
}
/*******************************************************************************
* *
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
* *
*******************************************************************************/
package com.github.shadowsocks.database
import android.database.sqlite.SQLiteDatabase
import android.support.v7.preference.PreferenceManager
import android.text.TextUtils
import com.github.shadowsocks.App.Companion.app
import com.github.shadowsocks.utils.Key
import com.j256.ormlite.android.AndroidDatabaseConnection
import com.j256.ormlite.android.apptools.OrmLiteSqliteOpenHelper
import com.j256.ormlite.dao.Dao
import com.j256.ormlite.support.ConnectionSource
import com.j256.ormlite.table.TableUtils
object DBHelper : OrmLiteSqliteOpenHelper(app, Key.PROFILE, null, 24) {
@Suppress("UNCHECKED_CAST")
val profileDao: Dao<Profile, Int> by lazy { getDao(Profile::class.java) as Dao<Profile, Int> }
@Suppress("UNCHECKED_CAST")
val kvPairDao: Dao<KeyValuePair, String?> by lazy { getDao(KeyValuePair::class.java) as Dao<KeyValuePair, String?> }
override fun onCreate(database: SQLiteDatabase?, connectionSource: ConnectionSource?) {
TableUtils.createTable(connectionSource, Profile::class.java)
TableUtils.createTable(connectionSource, KeyValuePair::class.java)
}
private fun recreate(database: SQLiteDatabase?, connectionSource: ConnectionSource?) {
TableUtils.dropTable<Profile, Int>(connectionSource, Profile::class.java, true)
TableUtils.dropTable<KeyValuePair, String?>(connectionSource, KeyValuePair::class.java, true)
onCreate(database, connectionSource)
}
override fun onUpgrade(database: SQLiteDatabase?, connectionSource: ConnectionSource?,
oldVersion: Int, newVersion: Int) {
if (oldVersion < 7) {
recreate(database, connectionSource)
return
}
try {
if (oldVersion < 8) {
profileDao.executeRawNoArgs("ALTER TABLE `profile` ADD COLUMN udpdns SMALLINT;")
}
if (oldVersion < 9) {
profileDao.executeRawNoArgs("ALTER TABLE `profile` ADD COLUMN route VARCHAR DEFAULT 'all';")
} else if (oldVersion < 19) {
profileDao.executeRawNoArgs("UPDATE `profile` SET route = 'all' WHERE route IS NULL;")
}
if (oldVersion < 11) {
profileDao.executeRawNoArgs("ALTER TABLE `profile` ADD COLUMN ipv6 SMALLINT;")
}
if (oldVersion < 12) {
profileDao.executeRawNoArgs("BEGIN TRANSACTION;")
profileDao.executeRawNoArgs("ALTER TABLE `profile` RENAME TO `tmp`;")
TableUtils.createTable(connectionSource, Profile::class.java)
profileDao.executeRawNoArgs(
"INSERT INTO `profile`(id, name, host, localPort, remotePort, password, method, route, proxyApps, bypass," +
" udpdns, ipv6, individual) " +
"SELECT id, name, host, localPort, remotePort, password, method, route, 1 - global, bypass, udpdns, ipv6," +
" individual FROM `tmp`;")
profileDao.executeRawNoArgs("DROP TABLE `tmp`;")
profileDao.executeRawNoArgs("COMMIT;")
} else if (oldVersion < 13) {
profileDao.executeRawNoArgs("ALTER TABLE `profile` ADD COLUMN tx LONG;")
profileDao.executeRawNoArgs("ALTER TABLE `profile` ADD COLUMN rx LONG;")
profileDao.executeRawNoArgs("ALTER TABLE `profile` ADD COLUMN date VARCHAR;")
}
if (oldVersion < 15) {
if (oldVersion >= 12) profileDao.executeRawNoArgs("ALTER TABLE `profile` ADD COLUMN userOrder LONG;")
var i = 0L
val apps by lazy { app.packageManager.getInstalledApplications(0) }
for (profile in profileDao.queryForAll()) {
if (oldVersion < 14) {
val uidSet = profile.individual.split('|').filter(TextUtils::isDigitsOnly)
.map(String::toInt).toSet()
profile.individual = apps.filter { uidSet.contains(it.uid) }
.joinToString("\n") { it.packageName }
}
profile.userOrder = i
profileDao.update(profile)
i += 1
}
}
if (oldVersion < 16) {
profileDao.executeRawNoArgs(
"UPDATE `profile` SET route = 'bypass-lan-china' WHERE route = 'bypass-china'")
}
if (oldVersion < 21) {
profileDao.executeRawNoArgs("ALTER TABLE `profile` ADD COLUMN remoteDns VARCHAR DEFAULT '8.8.8.8';")
}
if (oldVersion < 17) {
profileDao.executeRawNoArgs("ALTER TABLE `profile` ADD COLUMN plugin VARCHAR;")
} else if (oldVersion < 22) {
// upgrade kcptun to SIP003 plugin
profileDao.executeRawNoArgs("BEGIN TRANSACTION;")
profileDao.executeRawNoArgs("ALTER TABLE `profile` RENAME TO `tmp`;")
TableUtils.createTable(connectionSource, Profile::class.java)
profileDao.executeRawNoArgs(
"INSERT INTO `profile`(id, name, host, localPort, remotePort, password, method, route, " +
"remoteDns, proxyApps, bypass, udpdns, ipv6, individual, tx, rx, date, userOrder, " +
"plugin) " +
"SELECT id, name, host, localPort, " +
"CASE WHEN kcp = 1 THEN kcpPort ELSE remotePort END, password, method, route, " +
"remoteDns, proxyApps, bypass, udpdns, ipv6, individual, tx, rx, date, userOrder, " +
"CASE WHEN kcp = 1 THEN 'kcptun ' || kcpcli ELSE NULL END FROM `tmp`;")
profileDao.executeRawNoArgs("DROP TABLE `tmp`;")
profileDao.executeRawNoArgs("COMMIT;")
}
if (oldVersion < 23) {
profileDao.executeRawNoArgs("BEGIN TRANSACTION;")
TableUtils.createTable(connectionSource, KeyValuePair::class.java)
profileDao.executeRawNoArgs("COMMIT;")
val old = PreferenceManager.getDefaultSharedPreferences(app)
kvPairDao.createOrUpdate(KeyValuePair(Key.id).put(old.getInt(Key.id, 0)))
kvPairDao.createOrUpdate(KeyValuePair(Key.tfo).put(old.getBoolean(Key.tfo, false)))
}
} catch (ex: Exception) {
app.track(ex)
recreate(database, connectionSource)
}
}
override fun onDowngrade(db: SQLiteDatabase?, oldVersion: Int, newVersion: Int) {
val connection = AndroidDatabaseConnection(db, true)
connectionSource.saveSpecialConnection(connection)
recreate(db, connectionSource)
connectionSource.clearSpecialConnection(connection)
}
}
/*******************************************************************************
* *
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
* *
*******************************************************************************/
package com.github.shadowsocks.database
import com.j256.ormlite.field.DataType
import com.j256.ormlite.field.DatabaseField
import java.io.ByteArrayOutputStream
import java.nio.ByteBuffer
class KeyValuePair() {
companion object {
const val TYPE_UNINITIALIZED = 0
const val TYPE_BOOLEAN = 1
const val TYPE_FLOAT = 2
const val TYPE_INT = 3
const val TYPE_LONG = 4
const val TYPE_STRING = 5
const val TYPE_STRING_SET = 6
}
@DatabaseField(id = true)
var key: String? = ""
@DatabaseField
var valueType: Int = TYPE_UNINITIALIZED
@DatabaseField(dataType = DataType.BYTE_ARRAY)
var value: ByteArray = ByteArray(0)
val boolean: Boolean?
get() = if (valueType == TYPE_BOOLEAN) ByteBuffer.wrap(value).get() != 0.toByte() else null
val float: Float?
get() = if (valueType == TYPE_FLOAT) ByteBuffer.wrap(value).float else null
val int: Int?
get() = if (valueType == TYPE_INT) ByteBuffer.wrap(value).int else null
val long: Long?
get() = if (valueType == TYPE_LONG) ByteBuffer.wrap(value).long else null
val string: String?
get() = if (valueType == TYPE_STRING) String(value) else null
val stringSet: MutableSet<String>?
get() = if (valueType == TYPE_STRING_SET) {
val buffer = ByteBuffer.wrap(value)
val result = HashSet<String>()
while (buffer.hasRemaining()) {
val chArr = ByteArray(buffer.int)
buffer.get(chArr)
result.add(String(chArr))
}
result
} else null
constructor(key: String?) : this() {
this.key = key
}
// putting null requires using DataStore
fun put(value: Boolean): KeyValuePair {
valueType = TYPE_BOOLEAN
this.value = ByteBuffer.allocate(1).put((if (value) 1 else 0).toByte()).array()
return this
}
fun put(value: Float): KeyValuePair {
valueType = TYPE_FLOAT
this.value = ByteBuffer.allocate(4).putFloat(value).array()
return this
}
fun put(value: Int): KeyValuePair {
valueType = TYPE_INT
this.value = ByteBuffer.allocate(4).putInt(value).array()
return this
}
fun put(value: Long): KeyValuePair {
valueType = TYPE_LONG
this.value = ByteBuffer.allocate(8).putLong(value).array()
return this
}
fun put(value: String): KeyValuePair {
valueType = TYPE_STRING
this.value = value.toByteArray()
return this
}
fun put(value: MutableSet<String>): KeyValuePair {
valueType = TYPE_STRING_SET
val stream = ByteArrayOutputStream()
for (v in value) {
stream.write(ByteBuffer.allocate(4).putInt(v.length).array())
stream.write(v.toByteArray())
}
this.value = stream.toByteArray()
return this
}
}
/*******************************************************************************
* *
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
* *
*******************************************************************************/
package com.github.shadowsocks.database
import android.net.Uri
import android.util.Base64
import android.util.Log
import com.github.shadowsocks.plugin.PluginConfiguration
import com.github.shadowsocks.preference.DataStore
import com.github.shadowsocks.utils.Key
import com.github.shadowsocks.utils.parsePort
import com.j256.ormlite.field.DataType
import com.j256.ormlite.field.DatabaseField
import java.net.URI
import java.util.*
class Profile {
companion object {
private const val TAG = "ShadowParser"
private val pattern = """(?i)ss://[-a-zA-Z0-9+&@#/%?=~_|!:,.;\[\]]*[-a-zA-Z0-9+&@#/%=~_|\[\]]""".toRegex()
private val userInfoPattern = "^(.+?):(.*)$".toRegex()
private val legacyPattern = "^(.+?):(.*)@(.+?):(\\d+?)$".toRegex()
fun findAll(data: CharSequence?) = pattern.findAll(data ?: "").map {
val uri = Uri.parse(it.value)
if (uri.userInfo == null) {
val match = legacyPattern.matchEntire(String(Base64.decode(uri.host, Base64.NO_PADDING)))
if (match != null) {
val profile = Profile()
profile.method = match.groupValues[1].toLowerCase()
profile.password = match.groupValues[2]
profile.host = match.groupValues[3]
profile.remotePort = match.groupValues[4].toInt()
profile.plugin = uri.getQueryParameter(Key.plugin)
profile.name = uri.fragment
profile
} else {
Log.e(TAG, "Unrecognized URI: ${it.value}")
null
}
} else {
val match = userInfoPattern.matchEntire(String(Base64.decode(uri.userInfo,
Base64.NO_PADDING or Base64.NO_WRAP or Base64.URL_SAFE)))
if (match != null) {
val profile = Profile()
profile.method = match.groupValues[1]
profile.password = match.groupValues[2]
// bug in Android: https://code.google.com/p/android/issues/detail?id=192855
val javaURI = URI(it.value)
profile.host = javaURI.host
if (profile.host.firstOrNull() == '[' && profile.host.lastOrNull() == ']')
profile.host = profile.host.substring(1, profile.host.length - 1)
profile.remotePort = javaURI.port
profile.plugin = uri.getQueryParameter(Key.plugin)
profile.name = uri.fragment ?: ""
profile
} else {
Log.e(TAG, "Unknown user info: ${it.value}")
null
}
}
}.filterNotNull()
}
@DatabaseField(generatedId = true)
var id: Int = 0
@DatabaseField
var name: String? = ""
@DatabaseField
var host: String = "198.199.101.152"
@DatabaseField
var remotePort: Int = 8388
@DatabaseField
var password: String = "u1rRWTssNv0p"
@DatabaseField
var method: String = "aes-256-cfb"
@DatabaseField
var route: String = "all"
@DatabaseField
var remoteDns: String = "8.8.8.8"
@DatabaseField
var proxyApps: Boolean = false
@DatabaseField
var bypass: Boolean = false
@DatabaseField
var udpdns: Boolean = false
@DatabaseField
var ipv6: Boolean = true
@DatabaseField(dataType = DataType.LONG_STRING)
var individual: String = ""
@DatabaseField
var tx: Long = 0
@DatabaseField
var rx: Long = 0
@DatabaseField
val date: Date = Date()
@DatabaseField
var userOrder: Long = 0
@DatabaseField
var plugin: String? = null
val formattedAddress get() = (if (host.contains(":")) "[%s]:%d" else "%s:%d").format(host, remotePort)
val formattedName get() = if (name.isNullOrEmpty()) formattedAddress else name!!
fun toUri(): Uri {
val builder = Uri.Builder()
.scheme("ss")
.encodedAuthority("%s@%s:%d".format(Locale.ENGLISH,
Base64.encodeToString("%s:%s".format(Locale.ENGLISH, method, password).toByteArray(),
Base64.NO_PADDING or Base64.NO_WRAP or Base64.URL_SAFE),
if (host.contains(':')) "[$host]" else host, remotePort))
val configuration = PluginConfiguration(plugin ?: "")
if (configuration.selected.isNotEmpty())
builder.appendQueryParameter(Key.plugin, configuration.selectedOptions.toString(false))
if (!name.isNullOrEmpty()) builder.fragment(name)
return builder.build()
}
override fun toString() = toUri().toString()
fun serialize() {
DataStore.putString(Key.name, name)
DataStore.putString(Key.host, host)
DataStore.putString(Key.remotePort, remotePort.toString())
DataStore.putString(Key.password, password)
DataStore.putString(Key.route, route)
DataStore.putString(Key.remoteDns, remoteDns)
DataStore.putString(Key.method, method)
DataStore.proxyApps = proxyApps
DataStore.bypass = bypass
DataStore.putBoolean(Key.udpdns, udpdns)
DataStore.putBoolean(Key.ipv6, ipv6)
DataStore.individual = individual
DataStore.plugin = plugin ?: ""
DataStore.remove(Key.dirty)
}
fun deserialize() {
// It's assumed that default values are never used, so 0/false/null is always used even if that isn't the case
name = DataStore.getString(Key.name) ?: ""
host = DataStore.getString(Key.host) ?: ""
remotePort = parsePort(DataStore.getString(Key.remotePort), 8388, 1)
password = DataStore.getString(Key.password) ?: ""
method = DataStore.getString(Key.method) ?: ""
route = DataStore.getString(Key.route) ?: ""
remoteDns = DataStore.getString(Key.remoteDns) ?: ""
proxyApps = DataStore.proxyApps
bypass = DataStore.bypass
udpdns = DataStore.getBoolean(Key.udpdns, false)
ipv6 = DataStore.getBoolean(Key.ipv6, false)
individual = DataStore.individual
plugin = DataStore.plugin
}
}
/*******************************************************************************
* *
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
* *
*******************************************************************************/
package com.github.shadowsocks.database
import android.util.Log
import com.github.shadowsocks.App.Companion.app
import com.github.shadowsocks.ProfilesFragment
object ProfileManager {
private const val TAG = "ProfileManager"
fun createProfile(p: Profile? = null): Profile {
val profile = p ?: Profile()
profile.id = 0
try {
val oldProfile = app.currentProfile
if (oldProfile != null) {
// Copy Feature Settings from old profile
profile.route = oldProfile.route
profile.ipv6 = oldProfile.ipv6
profile.proxyApps = oldProfile.proxyApps
profile.bypass = oldProfile.bypass
profile.individual = oldProfile.individual
profile.udpdns = oldProfile.udpdns
}
val last = DBHelper.profileDao.queryRaw(DBHelper.profileDao.queryBuilder().selectRaw("MAX(userOrder)")
.prepareStatementString()).firstResult
if (last != null && last.size == 1 && last[0] != null) profile.userOrder = last[0].toLong() + 1
DBHelper.profileDao.createOrUpdate(profile)
ProfilesFragment.instance?.profilesAdapter?.add(profile)
} catch (ex: Exception) {
Log.e(TAG, "addProfile", ex)
app.track(ex)
}
return profile
}
fun updateProfile(profile: Profile): Boolean = try {
DBHelper.profileDao.update(profile)
true
} catch (ex: Exception) {
Log.e(TAG, "updateProfile", ex)
app.track(ex)
false
}
fun getProfile(id: Int): Profile? = try {
DBHelper.profileDao.queryForId(id)
} catch (ex: Exception) {
Log.e(TAG, "getProfile", ex)
app.track(ex)
null
}
fun delProfile(id: Int): Boolean = try {
DBHelper.profileDao.deleteById(id)
ProfilesFragment.instance?.profilesAdapter?.removeId(id)
true
} catch (ex: Exception) {
Log.e(TAG, "delProfile", ex)
app.track(ex)
false
}
fun getFirstProfile(): Profile? = try {
val result = DBHelper.profileDao.query(DBHelper.profileDao.queryBuilder().limit(1L).prepare())
if (result.size == 1) result[0] else null
} catch (ex: Exception) {
Log.e(TAG, "getAllProfiles", ex)
app.track(ex)
null
}
fun getAllProfiles(): List<Profile>? = try {
DBHelper.profileDao.query(DBHelper.profileDao.queryBuilder().orderBy("userOrder", true).prepare())
} catch (ex: Exception) {
Log.e(TAG, "getAllProfiles", ex)
app.track(ex)
null
}
}
/*******************************************************************************
* *
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
* *
*******************************************************************************/
package com.github.shadowsocks.plugin
import android.content.pm.ResolveInfo
import android.os.Bundle
class NativePlugin(resolveInfo: ResolveInfo) : ResolvedPlugin(resolveInfo) {
init {
assert(resolveInfo.providerInfo != null)
}
override val metaData: Bundle get() = resolveInfo.providerInfo.metaData
override val packageName: String get() = resolveInfo.providerInfo.packageName
}
package com.github.shadowsocks.plugin
import com.github.shadowsocks.App.Companion.app
object NoPlugin : Plugin() {
override val id: String get() = ""
override val label: CharSequence get() = app.getText(com.github.shadowsocks.R.string.plugin_disabled)
}
/*******************************************************************************
* *
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
* *
*******************************************************************************/
package com.github.shadowsocks.plugin
import android.graphics.drawable.Drawable
abstract class Plugin {
abstract val id: String
abstract val label: CharSequence
open val icon: Drawable? get() = null
open val defaultConfig: String? get() = null
open val packageName: String get() = ""
open val trusted: Boolean get() = true
}
/*******************************************************************************
* *
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
* *
*******************************************************************************/
package com.github.shadowsocks.plugin
import android.util.Log
import com.github.shadowsocks.utils.Commandline
import java.util.*
class PluginConfiguration(val pluginsOptions: Map<String, PluginOptions>, val selected: String) {
private constructor(plugins: List<PluginOptions>) : this(
plugins.filter { it.id.isNotEmpty() }.associate { it.id to it },
if (plugins.isEmpty()) "" else plugins[0].id)
constructor(plugin: String) : this(plugin.split('\n').map { line ->
if (line.startsWith("kcptun ")) {
val opt = PluginOptions()
opt.id = "kcptun"
try {
val iterator = Commandline.translateCommandline(line).drop(1).iterator()
while (iterator.hasNext()) {
val option = iterator.next()
when {
option == "--nocomp" -> opt.put("nocomp", null)
option.startsWith("--") -> opt.put(option.substring(2), iterator.next())
else -> throw IllegalArgumentException("Unknown kcptun parameter: " + option)
}
}
} catch (exc: Exception) {
Log.w("PluginConfiguration", exc.message)
}
opt
} else PluginOptions(line)
})
fun getOptions(id: String): PluginOptions = if (id.isEmpty()) PluginOptions() else
pluginsOptions.get(id) ?: PluginOptions(id, PluginManager.fetchPlugins()[id]?.defaultConfig)
val selectedOptions: PluginOptions get() = getOptions(selected)
override fun toString(): String {
val result = LinkedList<PluginOptions>()
for ((id, opt) in pluginsOptions) if (id == this.selected) result.addFirst(opt) else result.addLast(opt)
if (!pluginsOptions.contains(selected)) result.addFirst(selectedOptions)
return result.joinToString("\n") { it.toString(false) }
}
}
/*******************************************************************************
* *
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
* *
*******************************************************************************/
package com.github.shadowsocks.plugin
import android.annotation.SuppressLint
import android.content.ContentResolver
import android.content.Intent
import android.content.pm.PackageManager
import android.content.pm.Signature
import android.net.Uri
import android.os.Bundle
import android.util.Base64
import android.util.Log
import com.github.shadowsocks.App.Companion.app
import com.github.shadowsocks.utils.Commandline
import eu.chainfire.libsuperuser.Shell
import java.io.File
import java.io.FileNotFoundException
object PluginManager {
/**
* Trusted signatures by the app. Third-party fork should add their public key to their fork if the developer wishes
* to publish or has published plugins for this app. You can obtain your public key by executing:
*
* $ keytool -export -alias key-alias -keystore /path/to/keystore.jks -rfc
*
* If you don't plan to publish any plugin but is developing/has developed some, it's not necessary to add your
* public key yet since it will also automatically trust packages signed by the same signatures, e.g. debug keys.
*/
val trustedSignatures by lazy {
app.info.signatures.toSet() +
Signature(Base64.decode( // @Mygod
"""
|MIIDWzCCAkOgAwIBAgIEUzfv8DANBgkqhkiG9w0BAQsFADBdMQswCQYDVQQGEwJD
|TjEOMAwGA1UECBMFTXlnb2QxDjAMBgNVBAcTBU15Z29kMQ4wDAYDVQQKEwVNeWdv
|ZDEOMAwGA1UECxMFTXlnb2QxDjAMBgNVBAMTBU15Z29kMCAXDTE0MDUwMjA5MjQx
|OVoYDzMwMTMwOTAyMDkyNDE5WjBdMQswCQYDVQQGEwJDTjEOMAwGA1UECBMFTXln
|b2QxDjAMBgNVBAcTBU15Z29kMQ4wDAYDVQQKEwVNeWdvZDEOMAwGA1UECxMFTXln
|b2QxDjAMBgNVBAMTBU15Z29kMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC
|AQEAjm5ikHoP3w6zavvZU5bRo6Birz41JL/nZidpdww21q/G9APA+IiJMUeeocy0
|L7/QY8MQZABVwNq79LXYWJBcmmFXM9xBPgDqQP4uh9JsvazCI9bvDiMn92mz9HiS
|Sg9V4KGg0AcY0r230KIFo7hz+2QBp1gwAAE97myBfA3pi3IzJM2kWsh4LWkKQMfL
|M6KDhpb4mdDQnHlgi4JWe3SYbLtpB6whnTqjHaOzvyiLspx1tmrb0KVxssry9KoX
|YQzl56scfE/QJX0jJ5qYmNAYRCb4PibMuNSGB2NObDabSOMAdT4JLueOcHZ/x9tw
|agGQ9UdymVZYzf8uqc+29ppKdQIDAQABoyEwHzAdBgNVHQ4EFgQUBK4uJ0cqmnho
|6I72VmOVQMvVCXowDQYJKoZIhvcNAQELBQADggEBABZQ3yNESQdgNJg+NRIcpF9l
|YSKZvrBZ51gyrC7/2ZKMpRIyXruUOIrjuTR5eaONs1E4HI/uA3xG1eeW2pjPxDnO
|zgM4t7EPH6QbzibihoHw1MAB/mzECzY8r11PBhDQlst0a2hp+zUNR8CLbpmPPqTY
|RSo6EooQ7+NBejOXysqIF1q0BJs8Y5s/CaTOmgbL7uPCkzArB6SS/hzXgDk5gw6v
|wkGeOtzcj1DlbUTvt1s5GlnwBTGUmkbLx+YUje+n+IBgMbohLUDYBtUHylRVgMsc
|1WS67kDqeJiiQZvrxvyW6CZZ/MIGI+uAkkj3DqJpaZirkwPgvpcOIrjZy0uFvQM=
""", Base64.DEFAULT)) +
Signature(Base64.decode( // @madeye
"""
|MIICQzCCAaygAwIBAgIETV9OhjANBgkqhkiG9w0BAQUFADBmMQswCQYDVQQGEwJjbjERMA8GA1UE
|CBMIU2hhbmdoYWkxDzANBgNVBAcTBlB1ZG9uZzEUMBIGA1UEChMLRnVkYW4gVW5pdi4xDDAKBgNV
|BAsTA1BQSTEPMA0GA1UEAxMGTWF4IEx2MB4XDTExMDIxOTA1MDA1NFoXDTM2MDIxMzA1MDA1NFow
|ZjELMAkGA1UEBhMCY24xETAPBgNVBAgTCFNoYW5naGFpMQ8wDQYDVQQHEwZQdWRvbmcxFDASBgNV
|BAoTC0Z1ZGFuIFVuaXYuMQwwCgYDVQQLEwNQUEkxDzANBgNVBAMTBk1heCBMdjCBnzANBgkqhkiG
|9w0BAQEFAAOBjQAwgYkCgYEAq6lA8LqdeEI+es9SDX85aIcx8LoL3cc//iRRi+2mFIWvzvZ+bLKr
|4Wd0rhu/iU7OeMm2GvySFyw/GdMh1bqh5nNPLiRxAlZxpaZxLOdRcxuvh5Nc5yzjM+QBv8ECmuvu
|AOvvT3UDmA0AMQjZqSCmxWIxc/cClZ/0DubreBo2st0CAwEAATANBgkqhkiG9w0BAQUFAAOBgQAQ
|Iqonxpwk2ay+Dm5RhFfZyG9SatM/JNFx2OdErU16WzuK1ItotXGVJaxCZv3u/tTwM5aaMACGED5n
|AvHaDGCWynY74oDAopM4liF/yLe1wmZDu6Zo/7fXrH+T03LBgj2fcIkUfN1AA4dvnBo8XWAm9VrI
|1iNuLIssdhDz3IL9Yg==
""", Base64.DEFAULT))
}
private val receiver by lazy { app.listenForPackageChanges { synchronized(this) { cachedPlugins = null } } }
private var cachedPlugins: Map<String, Plugin>? = null
fun fetchPlugins(): Map<String, Plugin> {
receiver
return synchronized(this) {
if (cachedPlugins == null) {
val pm = app.packageManager
cachedPlugins = (pm.queryIntentContentProviders(Intent(PluginContract.ACTION_NATIVE_PLUGIN),
PackageManager.GET_META_DATA).map { NativePlugin(it) } + NoPlugin).associate { it.id to it }
}
cachedPlugins!!
}
}
private fun buildUri(id: String) = Uri.Builder()
.scheme(PluginContract.SCHEME)
.authority(PluginContract.AUTHORITY)
.path('/' + id)
.build()
fun buildIntent(id: String, action: String): Intent = Intent(action, buildUri(id))
// the following parts are meant to be used by :bg
@Throws(Throwable::class)
fun init(options: PluginOptions): String? {
if (options.id.isEmpty()) return null
var throwable: Throwable? = null
try {
val path = initNative(options)
if (path != null) return path
} catch (t: Throwable) {
t.printStackTrace()
if (throwable == null) throwable = t
}
// add other plugin types here
throw if (throwable != null) throwable else
FileNotFoundException(app.getString(com.github.shadowsocks.R.string.plugin_unknown, options.id))
}
private fun initNative(options: PluginOptions): String? {
val providers = app.packageManager.queryIntentContentProviders(
Intent(PluginContract.ACTION_NATIVE_PLUGIN, buildUri(options.id)), 0)
assert(providers.size == 1)
val uri = Uri.Builder()
.scheme(ContentResolver.SCHEME_CONTENT)
.authority(providers[0].providerInfo.authority)
.build()
val cr = app.contentResolver
return try {
initNativeFast(cr, options, uri)
} catch (t: Throwable) {
t.printStackTrace()
Log.w("PluginManager", "Initializing native plugin fast mode failed. Falling back to slow mode.")
initNativeSlow(cr, options, uri)
}
}
private fun initNativeFast(cr: ContentResolver, options: PluginOptions, uri: Uri): String {
val out = Bundle()
out.putString(PluginContract.EXTRA_OPTIONS, options.id)
val result = cr.call(uri, PluginContract.METHOD_GET_EXECUTABLE, null, out)
.getString(PluginContract.EXTRA_ENTRY)
assert(File(result).canExecute())
return result
}
@SuppressLint("Recycle")
private fun initNativeSlow(cr: ContentResolver, options: PluginOptions, uri: Uri): String? {
var initialized = false
fun entryNotFound(): Nothing = throw IndexOutOfBoundsException("Plugin entry binary not found")
val list = ArrayList<String>()
val pluginDir = File(app.filesDir, "plugin")
(cr.query(uri, arrayOf(PluginContract.COLUMN_PATH, PluginContract.COLUMN_MODE), null, null, null)
?: return null).use { cursor ->
if (!cursor.moveToFirst()) entryNotFound()
pluginDir.deleteRecursively()
if (!pluginDir.mkdirs()) throw FileNotFoundException("Unable to create plugin directory")
val pluginDirPath = pluginDir.absolutePath + '/'
do {
val path = cursor.getString(0)
val file = File(pluginDir, path)
assert(file.absolutePath.startsWith(pluginDirPath))
cr.openInputStream(uri.buildUpon().path(path).build()).use { inStream ->
file.outputStream().use { outStream -> inStream.copyTo(outStream) }
}
list += Commandline.toString(arrayOf("chmod", cursor.getString(1), file.absolutePath))
if (path == options.id) initialized = true
} while (cursor.moveToNext())
}
if (!initialized) entryNotFound()
Shell.SH.run(list)
return File(pluginDir, options.id).absolutePath
}
}
/*******************************************************************************
* *
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
* *
*******************************************************************************/
package com.github.shadowsocks.plugin
import android.content.pm.ResolveInfo
import android.graphics.drawable.Drawable
import android.os.Bundle
import com.github.shadowsocks.App.Companion.app
abstract class ResolvedPlugin(protected val resolveInfo: ResolveInfo) : Plugin() {
protected abstract val metaData: Bundle
override val id: String by lazy { metaData.getString(PluginContract.METADATA_KEY_ID) }
override val label: CharSequence by lazy { resolveInfo.loadLabel(app.packageManager) }
override val icon: Drawable by lazy { resolveInfo.loadIcon(app.packageManager) }
override val defaultConfig: String by lazy { metaData.getString(PluginContract.METADATA_KEY_DEFAULT_CONFIG) }
override val packageName: String get() = resolveInfo.resolvePackageName
override val trusted by lazy { app.info.signatures.any(PluginManager.trustedSignatures::contains) }
}
/*******************************************************************************
* *
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
* *
*******************************************************************************/
package com.github.shadowsocks.preference
import android.app.Dialog
import android.content.ActivityNotFoundException
import android.content.Intent
import android.graphics.Typeface
import android.net.Uri
import android.os.Bundle
import android.provider.Settings
import android.support.design.widget.BottomSheetDialog
import android.support.v7.preference.PreferenceDialogFragmentCompat
import android.support.v7.widget.LinearLayoutManager
import android.support.v7.widget.RecyclerView
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import com.github.shadowsocks.R
class BottomSheetPreferenceDialogFragment : PreferenceDialogFragmentCompat() {
private inner class IconListViewHolder(val dialog: BottomSheetDialog, view: View) : RecyclerView.ViewHolder(view),
View.OnClickListener, View.OnLongClickListener {
private var index = 0
private val text1 = view.findViewById<TextView>(android.R.id.text1)
private val text2 = view.findViewById<TextView>(android.R.id.text2)
private val icon = view.findViewById<ImageView>(android.R.id.icon)
init {
view.setOnClickListener(this)
view.setOnLongClickListener(this)
}
fun bind(i: Int, selected: Boolean = false) {
text1.text = preference.entries[i]
text2.text = preference.entryValues[i]
val typeface = if (selected) Typeface.BOLD else Typeface.NORMAL
text1.setTypeface(null, typeface)
text2.setTypeface(null, typeface)
text2.visibility = if (preference.entryValues[i].isNotEmpty() &&
preference.entries[i] != preference.entryValues[i]) View.VISIBLE else View.GONE
icon.setImageDrawable(preference.entryIcons?.get(i))
index = i
}
override fun onClick(p0: View?) {
clickedIndex = index
dialog.dismiss()
}
override fun onLongClick(p0: View?): Boolean {
val pn = preference.entryPackageNames?.get(index) ?: return false
return try {
startActivity(Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, Uri.Builder()
.scheme("package")
.opaquePart(pn)
.build()))
true
} catch (_: ActivityNotFoundException) {
false
}
}
}
private inner class IconListAdapter(private val dialog: BottomSheetDialog) :
RecyclerView.Adapter<IconListViewHolder>() {
override fun getItemCount(): Int = preference.entries.size
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = IconListViewHolder(dialog,
LayoutInflater.from(parent.context).inflate(R.layout.icon_list_item_2, parent, false))
override fun onBindViewHolder(holder: IconListViewHolder, position: Int) {
if (preference.selectedEntry < 0) holder.bind(position) else when (position) {
0 -> holder.bind(preference.selectedEntry, true)
in preference.selectedEntry + 1 .. Int.MAX_VALUE -> holder.bind(position)
else -> holder.bind(position - 1)
}
}
}
private val preference by lazy { getPreference() as IconListPreference }
private var clickedIndex = -1
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val activity = activity
val dialog = BottomSheetDialog(activity!!, theme)
val recycler = RecyclerView(activity)
recycler.setHasFixedSize(true)
recycler.layoutManager = LinearLayoutManager(activity)
recycler.adapter = IconListAdapter(dialog)
recycler.layoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
dialog.setContentView(recycler)
return dialog
}
override fun onDialogClosed(positiveResult: Boolean) {
if (clickedIndex >= 0 && clickedIndex != preference.selectedEntry) {
val value = preference.entryValues[clickedIndex].toString()
if (preference.callChangeListener(value)) preference.value = value
}
}
}
/*******************************************************************************
* *
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
* *
*******************************************************************************/
package com.github.shadowsocks.preference
import android.os.Binder
import android.support.v7.preference.PreferenceDataStore
import com.github.shadowsocks.database.DBHelper
import com.github.shadowsocks.database.KeyValuePair
import com.github.shadowsocks.utils.Key
import com.github.shadowsocks.utils.parsePort
import java.util.*
@Suppress("MemberVisibilityCanPrivate", "unused")
object DataStore : PreferenceDataStore() {
fun getBoolean(key: String?) = DBHelper.kvPairDao.queryForId(key)?.boolean
fun getFloat(key: String?) = DBHelper.kvPairDao.queryForId(key)?.float
fun getInt(key: String?) = DBHelper.kvPairDao.queryForId(key)?.int
fun getLong(key: String?) = DBHelper.kvPairDao.queryForId(key)?.long
fun getString(key: String?) = DBHelper.kvPairDao.queryForId(key)?.string
fun getStringSet(key: String?) = DBHelper.kvPairDao.queryForId(key)?.stringSet
override fun getBoolean(key: String?, defValue: Boolean) = getBoolean(key) ?: defValue
override fun getFloat(key: String?, defValue: Float) = getFloat(key) ?: defValue
override fun getInt(key: String?, defValue: Int) = getInt(key) ?: defValue
override fun getLong(key: String?, defValue: Long) = getLong(key) ?: defValue
override fun getString(key: String?, defValue: String?) = getString(key) ?: defValue
override fun getStringSet(key: String?, defValue: MutableSet<String>?) = getStringSet(key) ?: defValue
fun putBoolean(key: String?, value: Boolean?) = if (value == null) remove(key) else putBoolean(key, value)
fun putFloat(key: String?, value: Float?) = if (value == null) remove(key) else putFloat(key, value)
fun putInt(key: String?, value: Int?) = if (value == null) remove(key) else putInt(key, value)
fun putLong(key: String?, value: Long?) = if (value == null) remove(key) else putLong(key, value)
override fun putBoolean(key: String?, value: Boolean) {
DBHelper.kvPairDao.createOrUpdate(KeyValuePair(key).put(value))
fireChangeListener(key)
}
override fun putFloat(key: String?, value: Float) {
DBHelper.kvPairDao.createOrUpdate(KeyValuePair(key).put(value))
fireChangeListener(key)
}
override fun putInt(key: String?, value: Int) {
DBHelper.kvPairDao.createOrUpdate(KeyValuePair(key).put(value))
fireChangeListener(key)
}
override fun putLong(key: String?, value: Long) {
DBHelper.kvPairDao.createOrUpdate(KeyValuePair(key).put(value))
fireChangeListener(key)
}
override fun putString(key: String?, value: String?) = if (value == null) remove(key) else {
DBHelper.kvPairDao.createOrUpdate(KeyValuePair(key).put(value))
fireChangeListener(key)
}
override fun putStringSet(key: String?, values: MutableSet<String>?) = if (values == null) remove(key) else {
DBHelper.kvPairDao.createOrUpdate(KeyValuePair(key).put(values))
fireChangeListener(key)
}
fun remove(key: String?) {
DBHelper.kvPairDao.deleteById(key)
fireChangeListener(key)
}
private val listeners = HashSet<OnPreferenceDataStoreChangeListener>()
private fun fireChangeListener(key: String?) = listeners.forEach { it.onPreferenceDataStoreChanged(this, key) }
fun registerChangeListener(listener: OnPreferenceDataStoreChangeListener) = listeners.add(listener)
fun unregisterChangeListener(listener: OnPreferenceDataStoreChangeListener) = listeners.remove(listener)
// hopefully hashCode = mHandle doesn't change, currently this is true from KitKat to Nougat
private val userIndex by lazy { Binder.getCallingUserHandle().hashCode() }
private fun getLocalPort(key: String, default: Int): Int {
val value = getInt(key)
return if (value != null) {
putString(key, value.toString())
value
} else parsePort(getString(key), default + userIndex)
}
var profileId: Int
get() = getInt(Key.id) ?: 0
set(value) = putInt(Key.id, value)
var serviceMode: String
get() = getString(Key.serviceMode) ?: Key.modeVpn
set(value) = putString(Key.serviceMode, value)
var portProxy: Int
get() = getLocalPort(Key.portProxy, 1080)
set(value) = putString(Key.portProxy, value.toString())
var portLocalDns: Int
get() = getLocalPort(Key.portLocalDns, 5450)
set(value) = putString(Key.portLocalDns, value.toString())
var portTransproxy: Int
get() = getLocalPort(Key.portTransproxy, 8200)
set(value) = putString(Key.portTransproxy, value.toString())
var proxyApps: Boolean
get() = getBoolean(Key.proxyApps) ?: false
set(value) = putBoolean(Key.proxyApps, value)
var bypass: Boolean
get() = getBoolean(Key.bypass) ?: false
set(value) = putBoolean(Key.bypass, value)
var individual: String
get() = getString(Key.individual) ?: ""
set(value) = putString(Key.individual, value)
var plugin: String
get() = getString(Key.plugin) ?: ""
set(value) = putString(Key.plugin, value)
var dirty: Boolean
get() = getBoolean(Key.dirty) ?: false
set(value) = putBoolean(Key.dirty, value)
fun initGlobal() {
// temporary workaround for support lib bug
if (getString(Key.serviceMode) == null) serviceMode = serviceMode
if (getString(Key.portProxy) == null) portProxy = portProxy
if (getString(Key.portLocalDns) == null) portLocalDns = portLocalDns
if (getString(Key.portTransproxy) == null) portTransproxy = portTransproxy
}
}
/*******************************************************************************
* *
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
* *
*******************************************************************************/
package com.github.shadowsocks.preference
import android.content.Context
import android.graphics.drawable.Drawable
import android.support.v7.preference.ListPreference
import android.util.AttributeSet
class IconListPreference(context: Context, attrs: AttributeSet? = null) : ListPreference(context, attrs) {
var entryIcons: Array<Drawable?>? = null
val selectedEntry: Int get() = entryValues.indexOf(value)
val entryIcon: Drawable? get() = try {
entryIcons?.get(selectedEntry)
} catch (_: ArrayIndexOutOfBoundsException) {
null
}
// fun setEntryIcons(@ArrayRes entryIconsResId: Int) {
// val array = getContext().getResources().obtainTypedArray(entryIconsResId)
// entryIcons = Array(array.length(), { i -> array.getDrawable(i) })
// array.recycle()
// }
var unknownValueSummary: String? = null
var entryPackageNames: Array<String>? = null
private var listener: OnPreferenceChangeListener? = null
override fun getOnPreferenceChangeListener(): OnPreferenceChangeListener? = listener
override fun setOnPreferenceChangeListener(listener: OnPreferenceChangeListener?) {
this.listener = listener
}
init {
super.setOnPreferenceChangeListener({ preference, newValue ->
val listener = listener
if (listener == null || listener.onPreferenceChange(preference, newValue)) {
value = newValue.toString()
checkSummary()
if (entryIcons != null) icon = entryIcon
true
} else false
})
// val a = context.obtainStyledAttributes(attrs, R.styleable.IconListPreference)
// val entryIconsResId: Int = a.getResourceId(R.styleable.IconListPreference_entryIcons, -1)
// if (entryIconsResId != -1) entryIcons = entryIconsResId
// a.recycle()
}
fun checkSummary() {
val unknownValueSummary = unknownValueSummary
if (unknownValueSummary != null) summary = if (selectedEntry < 0) unknownValueSummary.format(value) else "%s"
}
fun init() {
icon = entryIcon
}
override fun onSetInitialValue(restoreValue: Boolean, defaultValue: Any?) {
super.onSetInitialValue(restoreValue, defaultValue)
init()
}
}
/*******************************************************************************
* *
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
* *
*******************************************************************************/
package com.github.shadowsocks.preference
import android.support.v7.preference.PreferenceDataStore
interface OnPreferenceDataStoreChangeListener {
fun onPreferenceDataStoreChanged(store: PreferenceDataStore, key: String?)
}
/*******************************************************************************
* *
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
* *
*******************************************************************************/
package com.github.shadowsocks.preference
import android.support.v7.app.AlertDialog
import android.view.View
import android.widget.EditText
import com.github.shadowsocks.ProfileConfigActivity
import com.github.shadowsocks.plugin.PluginContract
import com.github.shadowsocks.plugin.PluginManager
import com.takisoft.fix.support.v7.preference.EditTextPreferenceDialogFragmentCompat
class PluginConfigurationDialogFragment : EditTextPreferenceDialogFragmentCompat() {
companion object {
const val PLUGIN_ID_FRAGMENT_TAG =
"com.github.shadowsocks.preference.PluginConfigurationDialogFragment.PLUGIN_ID"
}
private lateinit var editText: EditText
override fun onPrepareDialogBuilder(builder: AlertDialog.Builder) {
super.onPrepareDialogBuilder(builder)
val intent = PluginManager.buildIntent(arguments!!.getString(PLUGIN_ID_FRAGMENT_TAG),
PluginContract.ACTION_HELP)
if (intent.resolveActivity(activity!!.packageManager) != null) builder.setNeutralButton("?", { _, _ ->
activity!!.startActivityForResult(intent.putExtra(PluginContract.EXTRA_OPTIONS, editText.text.toString()),
ProfileConfigActivity.REQUEST_CODE_PLUGIN_HELP)
})
}
override fun onBindDialogView(view: View) {
super.onBindDialogView(view)
editText = view.findViewById(android.R.id.edit)
}
}
/*******************************************************************************
* *
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
* *
*******************************************************************************/
package com.github.shadowsocks.tasker
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import com.github.shadowsocks.App.Companion.app
import com.github.shadowsocks.database.ProfileManager
class ActionListener : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
val settings = Settings.fromIntent(intent)
var changed = false
if (ProfileManager.getProfile(settings.profileId) != null) {
app.switchProfile(settings.profileId)
changed = true
}
if (settings.switchOn) {
app.startService()
if (changed) app.reloadService()
} else app.stopService()
}
}
/*******************************************************************************
* *
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
* *
*******************************************************************************/
package com.github.shadowsocks.tasker
import android.app.Activity
import android.content.res.Resources
import android.os.Build
import android.os.Bundle
import android.support.v7.app.AppCompatActivity
import android.support.v7.widget.DefaultItemAnimator
import android.support.v7.widget.LinearLayoutManager
import android.support.v7.widget.RecyclerView
import android.support.v7.widget.Toolbar
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.CheckedTextView
import android.widget.Switch
import com.github.shadowsocks.R
import com.github.shadowsocks.database.Profile
import com.github.shadowsocks.database.ProfileManager
class ConfigActivity : AppCompatActivity() {
inner class ProfileViewHolder(view: View) : RecyclerView.ViewHolder(view), View.OnClickListener {
private var item: Profile? = null
private val text = itemView.findViewById<CheckedTextView>(android.R.id.text1)
init {
val typedArray = obtainStyledAttributes(intArrayOf(android.R.attr.selectableItemBackground))
view.setBackgroundResource(typedArray.getResourceId(0, 0))
typedArray.recycle()
itemView.setOnClickListener(this)
}
fun bindDefault() {
item = null
text.setText(R.string.profile_default)
text.isChecked = taskerOption.profileId < 0
}
fun bind(item: Profile) {
this.item = item
text.text = item.formattedName
text.isChecked = taskerOption.profileId == item.id
}
override fun onClick(v: View?) {
taskerOption.switchOn = switch.isChecked
val item = item
taskerOption.profileId = item?.id ?: -1
setResult(Activity.RESULT_OK, taskerOption.toIntent(this@ConfigActivity))
finish()
}
}
inner class ProfilesAdapter : RecyclerView.Adapter<ProfileViewHolder>() {
internal val profiles = ProfileManager.getAllProfiles()?.toMutableList() ?: mutableListOf()
private val name = "select_dialog_singlechoice_" + (if (Build.VERSION.SDK_INT >= 21) "material" else "holo")
override fun onBindViewHolder(holder: ProfileViewHolder, position: Int) =
if (position == 0) holder.bindDefault() else holder.bind(profiles[position - 1])
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ProfileViewHolder = ProfileViewHolder(
LayoutInflater.from(parent.context).inflate(Resources.getSystem()
.getIdentifier(name, "layout", "android"), parent, false))
override fun getItemCount(): Int = 1 + profiles.size
}
private lateinit var taskerOption: Settings
private lateinit var switch: Switch
private val profilesAdapter = ProfilesAdapter()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
try {
taskerOption = Settings.fromIntent(intent!!)
} catch (_: Exception) {
finish()
return
}
setContentView(R.layout.layout_tasker)
val toolbar = findViewById<Toolbar>(R.id.toolbar)
toolbar.setTitle(R.string.app_name)
toolbar.setNavigationIcon(R.drawable.ic_navigation_close)
toolbar.setNavigationOnClickListener { finish() }
switch = findViewById(R.id.serviceSwitch)
switch.isChecked = taskerOption.switchOn
val profilesList = findViewById<RecyclerView>(R.id.list)
val lm = LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false)
profilesList.layoutManager = lm
profilesList.itemAnimator = DefaultItemAnimator()
profilesList.adapter = profilesAdapter
if (taskerOption.profileId >= 0)
lm.scrollToPosition(profilesAdapter.profiles.indexOfFirst { it.id == taskerOption.profileId } + 1)
}
}
/*******************************************************************************
* *
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
* *
*******************************************************************************/
package com.github.shadowsocks.tasker
import android.content.Context
import android.content.Intent
import android.os.Bundle
import com.github.shadowsocks.R
import com.github.shadowsocks.database.ProfileManager
import com.twofortyfouram.locale.api.Intent as ApiIntent
class Settings(bundle: Bundle) {
companion object {
private val KEY_SWITCH_ON = "switch_on"
private val KEY_PROFILE_ID = "profile_id"
fun fromIntent(intent: Intent) = Settings(intent.getBundleExtra(ApiIntent.EXTRA_BUNDLE))
}
var switchOn: Boolean = bundle.getBoolean(KEY_SWITCH_ON, true)
var profileId: Int = bundle.getInt(KEY_PROFILE_ID, -1)
fun toIntent(context: Context): Intent {
val bundle = Bundle()
if (!switchOn) bundle.putBoolean(KEY_SWITCH_ON, false)
if (profileId >= 0) bundle.putInt(KEY_PROFILE_ID, profileId)
val profile = ProfileManager.getProfile(profileId)
return Intent().putExtra(ApiIntent.EXTRA_BUNDLE, bundle).putExtra(ApiIntent.EXTRA_STRING_BLURB,
if (profile != null)
context.getString(if (switchOn) R.string.start_service else R.string.stop_service, profile.formattedName)
else context.getString(if (switchOn) R.string.start_service_default else R.string.stop))
}
}
package com.github.shadowsocks.utils;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.StringTokenizer;
/**
* Commandline objects help handling command lines specifying processes to
* execute.
*
* The class can be used to define a command line as nested elements or as a
* helper to define a command line by an application.
* <p>
* <code>
* &lt;someelement&gt;<br>
* &nbsp;&nbsp;&lt;acommandline executable="/executable/to/run"&gt;<br>
* &nbsp;&nbsp;&nbsp;&nbsp;&lt;argument value="argument 1" /&gt;<br>
* &nbsp;&nbsp;&nbsp;&nbsp;&lt;argument line="argument_1 argument_2 argument_3" /&gt;<br>
* &nbsp;&nbsp;&nbsp;&nbsp;&lt;argument value="argument 4" /&gt;<br>
* &nbsp;&nbsp;&lt;/acommandline&gt;<br>
* &lt;/someelement&gt;<br>
* </code>
*
* Based on: https://github.com/apache/ant/blob/588ce1f/src/main/org/apache/tools/ant/types/Commandline.java
*
* Adds support for escape character '\'.
*
* @author Mygod
*/
public final class Commandline {
private Commandline() { }
/**
* Quote the parts of the given array in way that makes them
* usable as command line arguments.
* @param args the list of arguments to quote.
* @return empty string for null or no command, else every argument split
* by spaces and quoted by quoting rules.
*/
public static String toString(Iterable<String> args) {
// empty path return empty string
if (args == null) {
return "";
}
// path containing one or more elements
final StringBuilder result = new StringBuilder();
for (String arg : args) {
if (result.length() > 0) result.append(' ');
for (int j = 0; j < arg.length(); ++j) {
char ch = arg.charAt(j);
switch (ch) {
case ' ': case '\\': case '"': case '\'': result.append('\\'); // intentionally no break
default: result.append(ch);
}
}
}
return result.toString();
}
/**
* Quote the parts of the given array in way that makes them
* usable as command line arguments.
* @param args the list of arguments to quote.
* @return empty string for null or no command, else every argument split
* by spaces and quoted by quoting rules.
*/
public static String toString(String[] args) {
return toString(Arrays.asList(args)); // thanks to Java, arrays aren't iterable
}
/**
* Crack a command line.
* @param toProcess the command line to process.
* @return the command line broken into strings.
* An empty or null toProcess parameter results in a zero sized array.
*/
public static String[] translateCommandline(String toProcess) {
if (toProcess == null || toProcess.length() == 0) {
//no command? no string
return new String[0];
}
// parse with a simple finite state machine
final int normal = 0;
final int inQuote = 1;
final int inDoubleQuote = 2;
int state = normal;
final StringTokenizer tok = new StringTokenizer(toProcess, "\\\"\' ", true);
final ArrayList<String> result = new ArrayList<>();
final StringBuilder current = new StringBuilder();
boolean lastTokenHasBeenQuoted = false;
boolean lastTokenIsSlash = false;
while (tok.hasMoreTokens()) {
String nextTok = tok.nextToken();
switch (state) {
case inQuote:
if ("\'".equals(nextTok)) {
lastTokenHasBeenQuoted = true;
state = normal;
} else {
current.append(nextTok);
}
break;
case inDoubleQuote:
if ("\"".equals(nextTok)) {
if (lastTokenIsSlash) {
current.append(nextTok);
lastTokenIsSlash = false;
} else {
lastTokenHasBeenQuoted = true;
state = normal;
}
} else if ("\\".equals(nextTok)) {
if (lastTokenIsSlash) {
current.append(nextTok);
lastTokenIsSlash = false;
} else lastTokenIsSlash = true;
} else {
if (lastTokenIsSlash) {
current.append("\\"); // unescaped
lastTokenIsSlash = false;
}
current.append(nextTok);
}
break;
default:
if (lastTokenIsSlash) {
current.append(nextTok);
lastTokenIsSlash = false;
} else if ("\\".equals(nextTok)) lastTokenIsSlash = true; else if ("\'".equals(nextTok)) {
state = inQuote;
} else if ("\"".equals(nextTok)) {
state = inDoubleQuote;
} else if (" ".equals(nextTok)) {
if (lastTokenHasBeenQuoted || current.length() != 0) {
result.add(current.toString());
current.setLength(0);
}
} else {
current.append(nextTok);
}
lastTokenHasBeenQuoted = false;
break;
}
}
if (lastTokenHasBeenQuoted || current.length() != 0) {
result.add(current.toString());
}
if (state == inQuote || state == inDoubleQuote) {
throw new IllegalArgumentException("unbalanced quotes in " + toProcess);
}
if (lastTokenIsSlash) throw new IllegalArgumentException("escape character following nothing in " + toProcess);
return result.toArray(new String[result.size()]);
}
}
/*******************************************************************************
* *
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
* *
*******************************************************************************/
package com.github.shadowsocks.utils
import java.util.ArrayList
import java.util.Arrays
import java.util.StringTokenizer
/**
* Commandline objects help handling command lines specifying processes to
* execute.
*
* The class can be used to define a command line as nested elements or as a
* helper to define a command line by an application.
*
*
* `
* <someelement><br></br>
* &nbsp;&nbsp;<acommandline executable="/executable/to/run"><br></br>
* &nbsp;&nbsp;&nbsp;&nbsp;<argument value="argument 1" /><br></br>
* &nbsp;&nbsp;&nbsp;&nbsp;<argument line="argument_1 argument_2 argument_3" /><br></br>
* &nbsp;&nbsp;&nbsp;&nbsp;<argument value="argument 4" /><br></br>
* &nbsp;&nbsp;</acommandline><br></br>
* </someelement><br></br>
` *
*
* Based on: https://github.com/apache/ant/blob/588ce1f/src/main/org/apache/tools/ant/types/Commandline.java
*
* Adds support for escape character '\'.
*/
object Commandline {
/**
* Quote the parts of the given array in way that makes them
* usable as command line arguments.
* @param args the list of arguments to quote.
* @return empty string for null or no command, else every argument split
* by spaces and quoted by quoting rules.
*/
fun toString(args: Iterable<String>?): String {
// empty path return empty string
if (args == null) {
return ""
}
// path containing one or more elements
val result = StringBuilder()
for (arg in args) {
if (result.isNotEmpty()) result.append(' ')
(0 until arg.length)
.map { arg[it] }
.forEach {
when (it) {
' ', '\\', '"', '\'' -> {
result.append('\\') // intentionally no break
result.append(it)
}
else -> result.append(it)
}
}
}
return result.toString()
}
/**
* Quote the parts of the given array in way that makes them
* usable as command line arguments.
* @param args the list of arguments to quote.
* @return empty string for null or no command, else every argument split
* by spaces and quoted by quoting rules.
*/
fun toString(args: Array<String>): String {
return toString(Arrays.asList(*args)) // thanks to Java, arrays aren't iterable
}
/**
* Crack a command line.
* @param toProcess the command line to process.
* @return the command line broken into strings.
* An empty or null toProcess parameter results in a zero sized array.
*/
fun translateCommandline(toProcess: String?): Array<String> {
if (toProcess == null || toProcess.isEmpty()) {
//no command? no string
return arrayOf()
}
// parse with a simple finite state machine
val normal = 0
val inQuote = 1
val inDoubleQuote = 2
var state = normal
val tok = StringTokenizer(toProcess, "\\\"\' ", true)
val result = ArrayList<String>()
val current = StringBuilder()
var lastTokenHasBeenQuoted = false
var lastTokenIsSlash = false
while (tok.hasMoreTokens()) {
val nextTok = tok.nextToken()
when (state) {
inQuote -> if ("\'" == nextTok) {
lastTokenHasBeenQuoted = true
state = normal
} else {
current.append(nextTok)
}
inDoubleQuote -> if ("\"" == nextTok) {
if (lastTokenIsSlash) {
current.append(nextTok)
lastTokenIsSlash = false
} else {
lastTokenHasBeenQuoted = true
state = normal
}
} else if ("\\" == nextTok) {
lastTokenIsSlash = if (lastTokenIsSlash) {
current.append(nextTok)
false
} else
true
} else {
if (lastTokenIsSlash) {
current.append("\\") // unescaped
lastTokenIsSlash = false
}
current.append(nextTok)
}
else -> {
if (lastTokenIsSlash) {
current.append(nextTok)
lastTokenIsSlash = false
} else if ("\\" == nextTok)
lastTokenIsSlash = true
else if ("\'" == nextTok) {
state = inQuote
} else if ("\"" == nextTok) {
state = inDoubleQuote
} else if (" " == nextTok) {
if (lastTokenHasBeenQuoted || current.isNotEmpty()) {
result.add(current.toString())
current.setLength(0)
}
} else {
current.append(nextTok)
}
lastTokenHasBeenQuoted = false
}
}
}
if (lastTokenHasBeenQuoted || current.isNotEmpty()) {
result.add(current.toString())
}
if (state == inQuote || state == inDoubleQuote) {
throw IllegalArgumentException("unbalanced quotes in " + toProcess)
}
if (lastTokenIsSlash) throw IllegalArgumentException("escape character following nothing in " + toProcess)
return result.toTypedArray()
}
}
/*******************************************************************************
* *
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
* *
*******************************************************************************/
package com.github.shadowsocks.utils
object Key {
const val PROFILE = "profile.db"
const val id = "profileId"
const val name = "profileName"
const val individual = "Proxyed"
const val serviceMode = "serviceMode"
const val modeProxy = "proxy"
const val modeVpn = "vpn"
const val modeTransproxy = "transproxy"
const val portProxy = "portProxy"
const val portLocalDns = "portLocalDns"
const val portTransproxy = "portTransproxy"
const val route = "route"
const val isAutoConnect = "isAutoConnect"
const val proxyApps = "isProxyApps"
const val bypass = "isBypassApps"
const val udpdns = "isUdpDns"
const val ipv6 = "isIpv6"
const val host = "proxy"
const val password = "sitekey"
const val method = "encMethod"
const val remotePort = "remotePortNum"
const val remoteDns = "remoteDns"
const val plugin = "plugin"
const val pluginConfigure = "plugin.configure"
const val dirty = "profileDirty"
const val tfo = "tcp_fastopen"
const val assetUpdateTime = "assetUpdateTime"
}
object Action {
const val SERVICE = "com.github.shadowsocks.SERVICE"
const val CLOSE = "com.github.shadowsocks.CLOSE"
const val RELOAD = "com.github.shadowsocks.RELOAD"
const val EXTRA_PROFILE_ID = "com.github.shadowsocks.EXTRA_PROFILE_ID"
}
/*
* Copyright 2010 Brave New Software Project, 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.github.shadowsocks.utils;
import android.annotation.TargetApi;
import android.content.Context;
import android.net.ConnectivityManager;
import android.net.LinkProperties;
import android.os.Build;
import java.lang.reflect.Method;
import java.net.InetAddress;
import java.util.ArrayList;
import java.util.Collection;
public class Dns {
public static String getDnsResolver(Context context) throws Exception {
Collection<InetAddress> dnsResolvers = getDnsResolvers(context);
if (dnsResolvers.isEmpty()) {
throw new Exception("Couldn't find an active DNS resolver");
}
String dnsResolver = dnsResolvers.iterator().next().toString();
if (dnsResolver.startsWith("/")) {
dnsResolver = dnsResolver.substring(1);
}
return dnsResolver;
}
private static Collection<InetAddress> getDnsResolvers(Context context) throws Exception {
ArrayList<InetAddress> addresses = new ArrayList<InetAddress>();
ConnectivityManager connectivityManager =
(ConnectivityManager)context.getSystemService(Context.CONNECTIVITY_SERVICE);
Class<?> LinkPropertiesClass = Class.forName("android.net.LinkProperties");
Method getActiveLinkPropertiesMethod = ConnectivityManager.class.getMethod("getActiveLinkProperties");
Object linkProperties = getActiveLinkPropertiesMethod.invoke(connectivityManager);
if (linkProperties != null) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
Method getDnsesMethod = LinkPropertiesClass.getMethod("getDnses");
Collection<?> dnses = (Collection<?>)getDnsesMethod.invoke(linkProperties);
for (Object dns : dnses) {
addresses.add((InetAddress)dns);
}
} else {
addresses.addAll(((LinkProperties) linkProperties).getDnsServers());
}
}
return addresses;
}
}
/*******************************************************************************
* *
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
* *
*******************************************************************************/
package com.github.shadowsocks.utils
import java.net.InetAddress
import java.util.*
class Subnet(val address: InetAddress, val prefixSize: Int) : Comparable<Subnet> {
companion object {
@Throws(IllegalArgumentException::class)
fun fromString(value: String): Subnet {
val parts = value.split('/')
val addr = parts[0].parseNumericAddress()
return when (parts.size) {
1 -> Subnet(addr, addr.address.size shl 3)
2 -> Subnet(addr, parts[1].toInt())
else -> throw IllegalArgumentException()
}
}
}
private val addressLength get() = address.address.size shl 3
init {
if (prefixSize < 0 || prefixSize > addressLength) throw IllegalArgumentException()
}
override fun toString(): String =
if (prefixSize == addressLength) address.hostAddress else address.hostAddress + '/' + prefixSize
private fun Byte.unsigned() = toInt() and 0xFF
override fun compareTo(other: Subnet): Int {
val addrThis = address.address
val addrThat = other.address.address
var result = addrThis.size.compareTo(addrThat.size) // IPv4 address goes first
if (result != 0) return result
for ((x, y) in addrThis zip addrThat) {
result = x.unsigned().compareTo(y.unsigned()) // undo sign extension of signed byte
if (result != 0) return result
}
return prefixSize.compareTo(other.prefixSize)
}
override fun equals(other: Any?): Boolean {
val that = other as? Subnet
return address == that?.address && prefixSize == that.prefixSize
}
override fun hashCode(): Int = Objects.hash(address, prefixSize)
}
/*******************************************************************************
* *
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
* *
*******************************************************************************/
package com.github.shadowsocks.utils
import eu.chainfire.libsuperuser.Shell
import java.io.File
object TcpFastOpen {
/**
* Is kernel version >= 3.7.1.
*/
val supported by lazy {
val match = """^(\d+)\.(\d+)\.(\d+)""".toRegex().find(System.getProperty("os.version"))
if (match == null) false else when (match.groupValues[1].toInt()) {
in Int.MIN_VALUE .. 2 -> false
3 -> when (match.groupValues[2].toInt()) {
in Int.MIN_VALUE .. 6 -> false
7 -> match.groupValues[3].toInt() >= 1
else -> true
}
else -> true
}
}
val sendEnabled: Boolean get() {
val file = File("/proc/sys/net/ipv4/tcp_fastopen")
return file.canRead() && file.bufferedReader().use { it.readText() }.trim().toInt() and 1 > 0
}
fun enabled(value: Boolean): String? = if (sendEnabled == value) null else Shell.run("su", arrayOf(
"if echo " + (if (value) 3 else 0) + " > /proc/sys/net/ipv4/tcp_fastopen; then",
" echo Success.",
"else",
" echo Failed.",
"fi"), null, true)?.joinToString("\n")
}
package com.github.shadowsocks.utils
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.os.Build
import android.support.v4.app.Fragment
import android.support.v4.app.FragmentManager
import android.support.v7.util.SortedList
import android.util.TypedValue
import com.github.shadowsocks.App.Companion.app
import java.lang.reflect.InvocationTargetException
import java.net.InetAddress
import java.net.URLConnection
private val isNumericMethod by lazy { InetAddress::class.java.getMethod("isNumeric", String::class.java) }
private val parseNumericAddressMethod by lazy {
InetAddress::class.java.getMethod("parseNumericAddress", String::class.java)
}
private val fieldChildFragmentManager by lazy {
val field = Fragment::class.java.getDeclaredField("mChildFragmentManager")
field.isAccessible = true
field
}
fun String.isNumericAddress(): Boolean = isNumericMethod.invoke(null, this) as Boolean
fun String.parseNumericAddress(): InetAddress = try {
parseNumericAddressMethod.invoke(null, this) as InetAddress
} catch (exc: InvocationTargetException) {
throw exc.cause ?: exc
}
fun parsePort(str: String?, default: Int, min: Int = 1025): Int {
val x = str?.toIntOrNull() ?: default
return if (x < min || x > 65535) default else x
}
fun broadcastReceiver(callback: (Context, Intent) -> Unit): BroadcastReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) = callback(context, intent)
}
/**
* Wrapper for kotlin.concurrent.thread that tracks uncaught exceptions.
*/
fun thread(start: Boolean = true, isDaemon: Boolean = false, contextClassLoader: ClassLoader? = null,
name: String? = null, priority: Int = -1, block: () -> Unit): Thread {
val thread = kotlin.concurrent.thread(false, isDaemon, contextClassLoader, name, priority, block)
thread.setUncaughtExceptionHandler(app::track)
if (start) thread.start()
return thread
}
val URLConnection.responseLength: Long
get() = if (Build.VERSION.SDK_INT >= 24) contentLengthLong else contentLength.toLong()
/**
* Based on: https://stackoverflow.com/a/15656428/2245107
*/
var Fragment.childFragManager: FragmentManager?
get() = childFragmentManager
set(value) = fieldChildFragmentManager.set(this, value)
private class SortedListIterable<out T>(private val list: SortedList<T>) : Iterable<T> {
override fun iterator(): Iterator<T> = SortedListIterator(list)
}
private class SortedListIterator<out T>(private val list: SortedList<T>) : Iterator<T> {
private var count = 0
override fun hasNext() = count < list.size()
override fun next(): T = list[count++]
}
fun <T> SortedList<T>.asIterable(): Iterable<T> = SortedListIterable(this)
/*******************************************************************************
* *
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
* *
*******************************************************************************/
package com.github.shadowsocks.widget
import android.content.Context
import android.support.v7.widget.GridLayout
import android.util.AttributeSet
import com.github.shadowsocks.R
/**
* Based on: http://stackoverflow.com/a/6212120/2245107
*/
class BoundedGridLayout(context: Context, attrs: AttributeSet) : GridLayout(context, attrs) {
private val boundedWidth: Int
private val boundedHeight: Int
init {
val arr = context.obtainStyledAttributes(attrs, R.styleable.BoundedGridLayout)
boundedWidth = arr.getDimensionPixelSize(R.styleable.BoundedGridLayout_bounded_width, 0)
boundedHeight = arr.getDimensionPixelSize(R.styleable.BoundedGridLayout_bounded_height, 0)
arr.recycle()
}
override fun onMeasure(widthSpec: Int, heightSpec: Int) = super.onMeasure(
if (boundedWidth <= 0 || boundedWidth >= MeasureSpec.getSize(widthSpec)) widthSpec
else MeasureSpec.makeMeasureSpec(boundedWidth, MeasureSpec.getMode(widthSpec)),
if (boundedHeight <= 0 || boundedHeight >= MeasureSpec.getSize(heightSpec)) heightSpec
else MeasureSpec.makeMeasureSpec(boundedHeight, MeasureSpec.getMode(heightSpec)))
}
/*******************************************************************************
* *
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
* *
*******************************************************************************/
package com.github.shadowsocks.widget
import android.support.design.widget.Snackbar
import android.view.View
import com.github.shadowsocks.R
/**
* @param view The view to find a parent from.
* @param undo Callback for undoing removals.
* @param commit Callback for committing removals.
* @tparam T Item type.
*/
class UndoSnackbarManager<in T>(private val view: View, private val undo: (List<Pair<Int, T>>) -> Unit,
commit: ((List<Pair<Int, T>>) -> Unit)? = null) {
private val recycleBin = ArrayList<Pair<Int, T>>()
private val removedCallback = object : Snackbar.Callback() {
override fun onDismissed(transientBottomBar: Snackbar?, event: Int) {
when (event) {
DISMISS_EVENT_SWIPE, DISMISS_EVENT_MANUAL, DISMISS_EVENT_TIMEOUT -> {
commit?.invoke(recycleBin)
recycleBin.clear()
}
}
last = null
}
}
private var last: Snackbar? = null
fun remove(items: Collection<Pair<Int, T>>) {
recycleBin.addAll(items)
val count = recycleBin.size
val snackbar = Snackbar
.make(view, view.resources.getQuantityString(R.plurals.removed, count, count), Snackbar.LENGTH_LONG)
.addCallback(removedCallback)
.setAction(R.string.undo, {
undo(recycleBin.reversed())
recycleBin.clear()
})
snackbar.show()
last = snackbar
}
fun remove(vararg items: Pair<Int, T>) = remove(items.toList())
fun flush() {
last?.dismiss()
last = null
}
}
APP_ABI := armeabi-v7a arm64-v8a x86
APP_PLATFORM := android-19
APP_STL := c++_static
NDK_TOOLCHAIN_VERSION := clang
...@@ -15,6 +15,7 @@ ...@@ -15,6 +15,7 @@
</item> </item>
</selector> </selector>
</item> </item>
<!-- android:start not available until API 23 -->
<item android:left="8dp"> <item android:left="8dp">
<shape android:shape="rectangle"> <shape android:shape="rectangle">
<solid android:color="@android:color/white" /> <solid android:color="@android:color/white" />
......
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<color xmlns:android="http://schemas.android.com/apk/res/android" android:color="@color/material_primary_300"/> <layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape android:shape="rectangle">
<solid android:color="@color/material_primary_300" />
</shape>
</item>
<item>
<selector>
<item android:state_pressed="true">
<shape android:shape="rectangle">
<solid android:color="#12000000" />
</shape>
</item>
</selector>
</item>
</layer-list>
<vector xmlns:android="http://schemas.android.com/apk/res/android" <vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp" android:width="24dp"
android:height="24dp" android:height="24dp"
android:autoMirrored="true"
android:viewportWidth="24.0" android:viewportWidth="24.0"
android:viewportHeight="24.0"> android:viewportHeight="24.0">
<path <path
......
<vector xmlns:android="http://schemas.android.com/apk/res/android" <vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp" android:width="24dp"
android:height="24dp" android:height="24dp"
android:autoMirrored="true"
android:viewportWidth="24.0" android:viewportWidth="24.0"
android:viewportHeight="24.0"> android:viewportHeight="24.0">
<path <path
......
<vector xmlns:android="http://schemas.android.com/apk/res/android" <vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp" android:width="24dp"
android:height="24dp" android:height="24dp"
android:autoMirrored="true"
android:viewportWidth="24.0" android:viewportWidth="24.0"
android:viewportHeight="24.0"> android:viewportHeight="24.0">
<path <path
......
<vector xmlns:android="http://schemas.android.com/apk/res/android" <vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp" android:width="24dp"
android:height="24dp" android:height="24dp"
android:autoMirrored="true"
android:viewportWidth="24.0" android:viewportWidth="24.0"
android:viewportHeight="24.0"> android:viewportHeight="24.0">
<path <path
......
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M16,1L4,1c-1.1,0 -2,0.9 -2,2v14h2L4,3h12L16,1zM19,5L8,5c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h11c1.1,0 2,-0.9 2,-2L21,7c0,-1.1 -0.9,-2 -2,-2zM19,21L8,21L8,7h11v14z"/>
</vector>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M19,2h-4.18C14.4,0.84 13.3,0 12,0c-1.3,0 -2.4,0.84 -2.82,2L5,2c-1.1,0 -2,0.9 -2,2v16c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2L21,4c0,-1.1 -0.9,-2 -2,-2zM12,2c0.55,0 1,0.45 1,1s-0.45,1 -1,1 -1,-0.45 -1,-1 0.45,-1 1,-1zM19,20L5,20L5,4h2v3h10L17,4h2v16z"/>
</vector>
<vector xmlns:android="http://schemas.android.com/apk/res/android" <vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp" android:width="24dp"
android:height="24dp" android:height="24dp"
android:autoMirrored="true"
android:viewportWidth="24.0" android:viewportWidth="24.0"
android:viewportHeight="24.0"> android:viewportHeight="24.0">
<path <path
......
<?xml version="1.0" encoding="utf-8"?>
<font-family xmlns:app="http://schemas.android.com/apk/res-auto"
app:fontProviderAuthority="com.google.android.gms.fonts"
app:fontProviderPackage="com.google.android.gms"
app:fontProviderQuery="Iceland"
app:fontProviderCerts="@array/com_google_android_gms_fonts_certs">
</font-family>
...@@ -14,28 +14,32 @@ ...@@ -14,28 +14,32 @@
android:background="@android:color/white" android:background="@android:color/white"
android:orientation="vertical" android:orientation="vertical"
tools:ignore="RtlSymmetry"> tools:ignore="RtlSymmetry">
<Switch android:checked="true"
android:id="@+id/onSwitch" <Switch
android:text="@string/on" android:id="@+id/onSwitch"
android:textSize="18sp" android:layout_width="match_parent"
android:textColor="?android:attr/textColorSecondary" android:layout_height="wrap_content"
android:paddingStart="60dp" android:checked="true"
android:paddingEnd="16dp" android:paddingBottom="12dp"
android:paddingTop="12dp" android:paddingEnd="@dimen/fastscroll__bubble_corner"
android:paddingBottom="12dp" android:paddingStart="60dp"
android:layout_width="match_parent" android:paddingTop="12dp"
android:layout_height="wrap_content"/> android:text="@string/on"
<Switch android:checked="true" android:textColor="?android:attr/textColorSecondary"
android:id="@+id/bypassSwitch" android:textSize="18sp"/>
android:text="@string/bypass_apps"
android:textSize="18sp" <Switch
android:textColor="?android:attr/textColorSecondary" android:id="@+id/bypassSwitch"
android:paddingStart="60dp" android:layout_width="match_parent"
android:paddingEnd="16dp" android:layout_height="wrap_content"
android:paddingTop="12dp" android:checked="true"
android:paddingBottom="12dp" android:paddingBottom="12dp"
android:layout_width="match_parent" android:paddingEnd="@dimen/fastscroll__bubble_corner"
android:layout_height="wrap_content"/> android:paddingStart="60dp"
android:paddingTop="12dp"
android:text="@string/bypass_apps"
android:textColor="?android:attr/textColorSecondary"
android:textSize="18sp"/>
</LinearLayout> </LinearLayout>
<FrameLayout android:layout_width="fill_parent" <FrameLayout android:layout_width="fill_parent"
android:layout_height="0dp" android:layout_height="0dp"
...@@ -48,7 +52,7 @@ ...@@ -48,7 +52,7 @@
android:layout_width="fill_parent" android:layout_width="fill_parent"
android:layout_height="fill_parent" android:layout_height="fill_parent"
android:id="@+id/list" android:id="@+id/list"
android:visibility="gone"/> tools:listitem="@layout/layout_apps_item"/>
<com.futuremind.recyclerviewfastscroll.FastScroller <com.futuremind.recyclerviewfastscroll.FastScroller
android:id="@+id/fastscroller" android:id="@+id/fastscroller"
android:orientation="vertical" android:orientation="vertical"
...@@ -57,7 +61,6 @@ ...@@ -57,7 +61,6 @@
android:layout_gravity="end" android:layout_gravity="end"
android:layout_marginTop="8dp" android:layout_marginTop="8dp"
android:layout_marginBottom="8dp" android:layout_marginBottom="8dp"
android:visibility="gone"
app:fastscroll__handleColor="@color/material_primary_500" app:fastscroll__handleColor="@color/material_primary_500"
app:fastscroll__bubbleColor="@color/material_primary_500" app:fastscroll__bubbleColor="@color/material_primary_500"
app:fastscroll__bubbleTextAppearance="@style/StyledScrollerTextAppearance"/> app:fastscroll__bubbleTextAppearance="@style/StyledScrollerTextAppearance"/>
......
...@@ -9,25 +9,25 @@ ...@@ -9,25 +9,25 @@
<ImageView <ImageView
android:id="@+id/itemicon" android:id="@+id/itemicon"
android:layout_width="60dp" android:layout_width="60dp"
android:layout_height="48dp" android:layout_height="52dp"
android:scaleType="fitCenter" android:paddingBottom="4dp"
android:paddingStart="8dp"
android:paddingEnd="8dp" android:paddingEnd="8dp"
android:paddingTop="2dp" android:paddingStart="8dp"
android:paddingBottom="2dp"/> android:paddingTop="4dp"
android:scaleType="fitCenter"/>
<Switch <Switch
android:id="@+id/itemcheck" android:id="@+id/itemcheck"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="fill_parent" android:layout_height="fill_parent"
android:gravity="center_vertical" android:layout_marginEnd="@dimen/fastscroll__bubble_corner"
android:layout_weight="1"
android:clickable="false"
android:ellipsize="end" android:ellipsize="end"
android:textSize="18sp"
android:textColor="?android:attr/textColorSecondary"
android:focusable="false" android:focusable="false"
android:focusableInTouchMode="false" android:focusableInTouchMode="false"
android:clickable="false" android:gravity="center_vertical"
android:layout_marginEnd="16dp"/> android:textColor="?android:attr/textColorSecondary"
android:textSize="18sp"/>
</LinearLayout> </LinearLayout>
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:orientation="vertical"> android:orientation="vertical">
...@@ -12,6 +13,7 @@ ...@@ -12,6 +13,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:paddingBottom="32dp" android:paddingBottom="32dp"
tools:listitem="@android:layout/simple_list_item_1"
android:clipToPadding="false"/> android:clipToPadding="false"/>
<com.futuremind.recyclerviewfastscroll.FastScroller <com.futuremind.recyclerviewfastscroll.FastScroller
android:id="@+id/fastscroller" android:id="@+id/fastscroller"
......
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent" xmlns:tools="http://schemas.android.com/tools"
android:layout_height="wrap_content"> android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView android:layout_width="match_parent" <ImageView android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical" android:orientation="vertical"
...@@ -10,12 +11,14 @@ ...@@ -10,12 +11,14 @@
android:src="@drawable/background_header"/> android:src="@drawable/background_header"/>
<TextView xmlns:android="http://schemas.android.com/apk/res/android" <TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/drawer_title" android:id="@+id/drawer_title"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_centerVertical="true" android:layout_centerVertical="true"
android:text="shadowsocks" android:fontFamily="@font/iceland"
android:gravity="center" android:gravity="center"
android:textSize="32sp" android:text="shadowsocks"
android:textColor="@android:color/white"/> android:textColor="@android:color/white"
android:textSize="32sp"
tools:ignore="HardcodedText"/>
</RelativeLayout> </RelativeLayout>
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:orientation="vertical"> android:orientation="vertical">
<include layout="@layout/toolbar_light_dark"/> <include layout="@layout/toolbar_light_dark"/>
<android.support.v7.widget.RecyclerView android:id="@+id/list" <android.support.v7.widget.RecyclerView android:id="@+id/list"
tools:listitem="@android:layout/simple_list_item_1"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="0dp" android:layout_height="0dp"
android:layout_weight="1" android:layout_weight="1"
......
...@@ -39,7 +39,7 @@ ...@@ -39,7 +39,7 @@
<TextView app:layout_column="1" android:id="@+id/tx" android:textSize="14sp" <TextView app:layout_column="1" android:id="@+id/tx" android:textSize="14sp"
android:textColor="?android:attr/textColorSecondary" android:ellipsize="marquee" android:textColor="?android:attr/textColorSecondary" android:ellipsize="marquee"
android:layout_width="0dp" android:layout_height="wrap_content" android:layout_width="0dp" android:layout_height="wrap_content"
app:layout_columnWeight="1" app:layout_gravity="fill_horizontal"/> app:layout_columnWeight="1" android:layout_gravity="fill_horizontal"/>
<TextView app:layout_column="2" android:textSize="14sp" <TextView app:layout_column="2" android:textSize="14sp"
android:textColor="?android:attr/textColorSecondary" android:textColor="?android:attr/textColorSecondary"
android:ellipsize="marquee" android:text="▲" android:ellipsize="marquee" android:text="▲"
...@@ -47,7 +47,7 @@ ...@@ -47,7 +47,7 @@
<TextView app:layout_column="3" android:id="@+id/txRate" android:textSize="14sp" <TextView app:layout_column="3" android:id="@+id/txRate" android:textSize="14sp"
android:textColor="?android:attr/textColorSecondary" android:ellipsize="marquee" android:textColor="?android:attr/textColorSecondary" android:ellipsize="marquee"
android:layout_width="0dp" android:layout_height="wrap_content" app:layout_columnWeight="1" android:layout_width="0dp" android:layout_height="wrap_content" app:layout_columnWeight="1"
app:layout_gravity="fill_horizontal" android:gravity="end"/> android:layout_gravity="fill_horizontal" android:gravity="end"/>
<TextView app:layout_row="1" app:layout_column="0" style="@style/TextAppearance.AppCompat.Body2" <TextView app:layout_row="1" app:layout_column="0" style="@style/TextAppearance.AppCompat.Body2"
android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_width="wrap_content" android:layout_height="wrap_content"
android:layout_marginEnd="24dp" android:layout_marginEnd="24dp"
...@@ -56,14 +56,14 @@ ...@@ -56,14 +56,14 @@
<TextView app:layout_row="1" app:layout_column="1" android:id="@+id/rx" android:textSize="14sp" <TextView app:layout_row="1" app:layout_column="1" android:id="@+id/rx" android:textSize="14sp"
android:textColor="?android:attr/textColorSecondary" android:ellipsize="marquee" android:textColor="?android:attr/textColorSecondary" android:ellipsize="marquee"
android:layout_width="0dp" android:layout_height="wrap_content" app:layout_columnWeight="1" android:layout_width="0dp" android:layout_height="wrap_content" app:layout_columnWeight="1"
app:layout_gravity="fill_horizontal" /> android:layout_gravity="fill_horizontal" />
<TextView app:layout_row="1" app:layout_column="2" android:textSize="14sp" <TextView app:layout_row="1" app:layout_column="2" android:textSize="14sp"
android:textColor="?android:attr/textColorSecondary" android:ellipsize="marquee" android:textColor="?android:attr/textColorSecondary" android:ellipsize="marquee"
android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="▼"/> android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="▼"/>
<TextView app:layout_row="1" app:layout_column="3" android:id="@+id/rxRate" android:textSize="14sp" <TextView app:layout_row="1" app:layout_column="3" android:id="@+id/rxRate" android:textSize="14sp"
android:textColor="?android:attr/textColorSecondary" android:ellipsize="marquee" android:textColor="?android:attr/textColorSecondary" android:ellipsize="marquee"
android:layout_width="0dp" android:layout_height="wrap_content" app:layout_columnWeight="1" android:layout_width="0dp" android:layout_height="wrap_content" app:layout_columnWeight="1"
app:layout_gravity="fill_horizontal" android:gravity="end"/> android:layout_gravity="fill_horizontal" android:gravity="end"/>
</com.github.shadowsocks.widget.BoundedGridLayout> </com.github.shadowsocks.widget.BoundedGridLayout>
</LinearLayout> </LinearLayout>
<com.github.jorgecastilloprz.FABProgressCircle android:id="@+id/fabProgressCircle" <com.github.jorgecastilloprz.FABProgressCircle android:id="@+id/fabProgressCircle"
......
...@@ -3,80 +3,96 @@ ...@@ -3,80 +3,96 @@
<LinearLayout <LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/content" android:id="@+id/content"
android:orientation="vertical"
android:background="@drawable/background_profile"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="@drawable/background_profile"
android:focusable="true" android:focusable="true"
android:nextFocusRight="@+id/edit"> android:nextFocusRight="@+id/edit"
android:orientation="vertical">
<Space
android:layout_width="match_parent"
android:layout_height="8dp"/>
<LinearLayout <LinearLayout
android:id="@+id/container"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="4dp" android:layout_marginLeft="16dp"
android:paddingLeft="16dp" >
android:paddingRight="4dp"
android:id="@+id/container">
<TextView <TextView
android:id="@android:id/text1" android:id="@android:id/text1"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center_vertical" android:layout_gravity="center_vertical"
android:layout_weight="1" android:layout_weight="1"
android:ellipsize="end"
android:gravity="center_vertical"
android:maxLines="2"
android:textAppearance="?android:attr/textAppearanceMedium" android:textAppearance="?android:attr/textAppearanceMedium"
android:textColor="?android:attr/textColorPrimary" android:textColor="?android:attr/textColorPrimary"
android:textStyle="bold" android:textStyle="bold"
android:gravity="center_vertical" tools:text="@string/profile_name"/>
android:maxLines="2"
android:ellipsize="end"/>
<android.support.v7.widget.AppCompatImageView <android.support.v7.widget.AppCompatImageView
android:id="@+id/share" android:id="@+id/share"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:padding="8dp"
android:layout_gravity="top" android:layout_gravity="top"
app:srcCompat="@drawable/ic_social_share"
android:background="?attr/selectableItemBackgroundBorderless" android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/share" android:contentDescription="@string/share"
android:focusable="true" android:focusable="true"
android:nextFocusLeft="@+id/container"/> android:nextFocusLeft="@+id/container"
android:padding="8dp"
app:srcCompat="@drawable/ic_social_share"/>
<android.support.v7.widget.AppCompatImageView <android.support.v7.widget.AppCompatImageView
android:id="@+id/edit" android:id="@+id/edit"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:padding="8dp"
android:layout_gravity="top" android:layout_gravity="top"
app:srcCompat="@drawable/ic_image_edit"
android:background="?attr/selectableItemBackgroundBorderless" android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/edit" android:contentDescription="@string/edit"
android:focusable="true"/> android:focusable="true"
android:padding="8dp"
app:srcCompat="@drawable/ic_image_edit"/>
</LinearLayout> </LinearLayout>
<RelativeLayout <RelativeLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:paddingLeft="16dp" android:layout_marginLeft="16dp"
android:paddingRight="4dp" >
android:layout_marginEnd="8dp"
android:layout_marginBottom="16dp">
<TextView <TextView
android:id="@android:id/text2" android:id="@android:id/text2"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_alignParentStart="true" android:layout_alignParentStart="true"
android:layout_toStartOf="@+id/traffic"
android:layout_marginEnd="8dp" android:layout_marginEnd="8dp"
android:maxLines="2" android:layout_toStartOf="@+id/traffic"
android:ellipsize="end" android:ellipsize="end"
android:maxLines="2"
tools:text="@string/profile_plugin"
android:textAppearance="?android:attr/textAppearanceSmall" android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="?android:attr/textColorSecondary"/> android:textColor="?android:attr/textColorSecondary"/>
<TextView <TextView
android:id="@+id/traffic" android:id="@+id/traffic"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:layout_alignParentEnd="true" android:layout_alignParentEnd="true"
android:layout_gravity="bottom"
android:layout_marginEnd="8dp"
android:ellipsize="end" android:ellipsize="end"
tools:text="@string/traffic"
android:textAppearance="?android:attr/textAppearanceSmall" android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="?android:attr/textColorSecondary"/> android:textColor="?android:attr/textColorSecondary"/>
</RelativeLayout> </RelativeLayout>
<Space
android:layout_width="match_parent"
android:layout_height="8dp"/>
</LinearLayout> </LinearLayout>
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical" android:orientation="vertical"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
...@@ -18,5 +19,6 @@ ...@@ -18,5 +19,6 @@
<android.support.v7.widget.RecyclerView android:id="@+id/list" <android.support.v7.widget.RecyclerView android:id="@+id/list"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
tools:itemview="@android:layout/select_dialog_singlechoice_material"
android:scrollbars="vertical"/> android:scrollbars="vertical"/>
</LinearLayout> </LinearLayout>
<menu xmlns:android="http://schemas.android.com/apk/res/android" <menu xmlns:tools="http://schemas.android.com/tools"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"> xmlns:app="http://schemas.android.com/apk/res-auto">
<item android:id="@+id/action_apply_all" <item android:id="@+id/action_apply_all"
android:title="@string/action_apply_all" android:title="@string/action_apply_all"
android:alphabeticShortcut="a" android:alphabeticShortcut="a"
android:numericShortcut="1" android:numericShortcut="1"
app:showAsAction="never"/> app:showAsAction="never"/>
<item android:id="@+id/action_export" <item
android:title="@string/action_export" android:id="@+id/action_export"
android:icon="@drawable/ic_content_copy" android:alphabeticShortcut="e"
app:showAsAction="ifRoom" android:icon="?attr/actionModeCopyDrawable"
android:alphabeticShortcut="e" android:numericShortcut="2"
android:numericShortcut="2"/> android:title="@string/action_export"
<item android:id="@+id/action_import" app:showAsAction="ifRoom"/>
android:title="@string/action_import" <item
android:icon="@drawable/ic_content_paste" android:id="@+id/action_import"
app:showAsAction="ifRoom" android:alphabeticShortcut="i"
android:alphabeticShortcut="i" android:icon="?attr/actionModePasteDrawable"
android:numericShortcut="3"/> android:numericShortcut="3"
android:title="@string/action_import"
app:showAsAction="ifRoom"/>
</menu> </menu>
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android" <menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"> xmlns:app="http://schemas.android.com/apk/res-auto">
<item android:title="@string/add_profile" <item
android:icon="@drawable/ic_action_note_add" android:alphabeticShortcut="a"
android:alphabeticShortcut="a" android:icon="@drawable/ic_action_note_add"
android:numericShortcut="0" android:numericShortcut="0"
app:showAsAction="always"> android:title="@string/add_profile"
app:showAsAction="always">
<menu> <menu>
<item android:id="@+id/action_scan_qr_code" <item
android:title="@string/add_profile_methods_scan_qr_code" android:id="@+id/action_scan_qr_code"
android:alphabeticShortcut="s" android:alphabeticShortcut="s"
android:numericShortcut="1"/> android:numericShortcut="1"
<item android:id="@+id/action_import" android:title="@string/add_profile_methods_scan_qr_code"/>
android:title="@string/action_import" <item
android:alphabeticShortcut="i" android:id="@+id/action_import"
android:numericShortcut="2"/> android:alphabeticShortcut="i"
<item android:id="@+id/action_manual_settings" android:numericShortcut="2"
android:title="@string/add_profile_methods_manual_settings" android:title="@string/action_import"/>
android:alphabeticShortcut="m" <item
android:numericShortcut="3"/> android:id="@+id/action_manual_settings"
android:alphabeticShortcut="m"
android:numericShortcut="3"
android:title="@string/add_profile_methods_manual_settings"/>
</menu> </menu>
</item> </item>
<item android:id="@+id/action_export" <item
android:title="@string/action_export" android:id="@+id/action_export"
android:alphabeticShortcut="e" android:alphabeticShortcut="e"
android:numericShortcut="1" android:icon="?attr/actionModeCopyDrawable"
android:icon="@drawable/ic_content_copy" android:numericShortcut="1"
app:showAsAction="always"/> android:title="@string/action_export"
app:showAsAction="always"/>
</menu> </menu>
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<string-array name="enc_method_entry"> <string-array name="enc_method_entry" translatable="false">
<item>RC4-MD5</item> <item>RC4-MD5</item>
<item>AES-128-CFB</item> <item>AES-128-CFB</item>
<item>AES-192-CFB</item> <item>AES-192-CFB</item>
...@@ -22,7 +22,7 @@ ...@@ -22,7 +22,7 @@
<item>XCHACHA20-IETF-POLY1305</item> <item>XCHACHA20-IETF-POLY1305</item>
</string-array> </string-array>
<string-array name="enc_method_value"> <string-array name="enc_method_value" translatable="false">
<item>rc4-md5</item> <item>rc4-md5</item>
<item>aes-128-cfb</item> <item>aes-128-cfb</item>
<item>aes-192-cfb</item> <item>aes-192-cfb</item>
...@@ -53,7 +53,7 @@ ...@@ -53,7 +53,7 @@
<item>@string/route_entry_chinalist</item> <item>@string/route_entry_chinalist</item>
<item>@string/custom_rules</item> <item>@string/custom_rules</item>
</string-array> </string-array>
<string-array name="route_value"> <string-array name="route_value" translatable="false">
<item>all</item> <item>all</item>
<item>bypass-lan</item> <item>bypass-lan</item>
<item>bypass-china</item> <item>bypass-china</item>
...@@ -63,7 +63,7 @@ ...@@ -63,7 +63,7 @@
<item>custom-rules</item> <item>custom-rules</item>
</string-array> </string-array>
<string-array name="bypass_private_route"> <string-array name="bypass_private_route" translatable="false">
<item>1.0.0.0/8</item> <item>1.0.0.0/8</item>
<item>2.0.0.0/7</item> <item>2.0.0.0/7</item>
<item>4.0.0.0/6</item> <item>4.0.0.0/6</item>
...@@ -221,7 +221,7 @@ ...@@ -221,7 +221,7 @@
<item>208.0.0.0/4</item> <item>208.0.0.0/4</item>
</string-array> </string-array>
<string-array name="acl_rule_templates"> <string-array name="acl_rule_templates" translatable="false">
<item>@string/acl_rule_templates_generic</item> <item>@string/acl_rule_templates_generic</item>
<item>@string/acl_rule_templates_domain</item> <item>@string/acl_rule_templates_domain</item>
<item>URL</item> <item>URL</item>
...@@ -232,7 +232,7 @@ ...@@ -232,7 +232,7 @@
<item>@string/service_mode_vpn</item> <item>@string/service_mode_vpn</item>
<item>@string/service_mode_transproxy</item> <item>@string/service_mode_transproxy</item>
</string-array> </string-array>
<string-array name="service_mode_values"> <string-array name="service_mode_values" translatable="false">
<item>proxy</item> <item>proxy</item>
<item>vpn</item> <item>vpn</item>
<item>transproxy</item> <item>transproxy</item>
......
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<declare-styleable name="BoundedView"> <declare-styleable name="BoundedGridLayout">
<attr name="bounded_width" format="dimension" /> <attr name="bounded_width" format="dimension" />
<attr name="bounded_height" format="dimension" /> <attr name="bounded_height" format="dimension" />
</declare-styleable> </declare-styleable>
......
...@@ -2,7 +2,6 @@ ...@@ -2,7 +2,6 @@
<resources> <resources>
<dimen name="profile_item_max_width">480dp</dimen> <dimen name="profile_item_max_width">480dp</dimen>
<dimen name="qr_code_size">250dp</dimen> <dimen name="qr_code_size">250dp</dimen>
<dimen name="margin_small">8dp</dimen>
<!-- TODO: Remove when this issue is fixed: https://github.com/JorgeCastilloPrz/FABProgressCircle/issues/19 --> <!-- TODO: Remove when this issue is fixed: https://github.com/JorgeCastilloPrz/FABProgressCircle/issues/19 -->
<dimen name="fab_margin_top">-44dp</dimen> <dimen name="fab_margin_top">-44dp</dimen>
</resources> </resources>
<?xml version="1.0" encoding="utf-8"?>
<resources>
<array name="com_google_android_gms_fonts_certs">
<item>@array/com_google_android_gms_fonts_certs_dev</item>
<item>@array/com_google_android_gms_fonts_certs_prod</item>
</array>
<string-array name="com_google_android_gms_fonts_certs_dev" translatable="false">
<item>
MIIEqDCCA5CgAwIBAgIJANWFuGx90071MA0GCSqGSIb3DQEBBAUAMIGUMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEQMA4GA1UEChMHQW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDEiMCAGCSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbTAeFw0wODA0MTUyMzM2NTZaFw0zNTA5MDEyMzM2NTZaMIGUMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEQMA4GA1UEChMHQW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDEiMCAGCSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbTCCASAwDQYJKoZIhvcNAQEBBQADggENADCCAQgCggEBANbOLggKv+IxTdGNs8/TGFy0PTP6DHThvbbR24kT9ixcOd9W+EaBPWW+wPPKQmsHxajtWjmQwWfna8mZuSeJS48LIgAZlKkpFeVyxW0qMBujb8X8ETrWy550NaFtI6t9+u7hZeTfHwqNvacKhp1RbE6dBRGWynwMVX8XW8N1+UjFaq6GCJukT4qmpN2afb8sCjUigq0GuMwYXrFVee74bQgLHWGJwPmvmLHC69EH6kWr22ijx4OKXlSIx2xT1AsSHee70w5iDBiK4aph27yH3TxkXy9V89TDdexAcKk/cVHYNnDBapcavl7y0RiQ4biu8ymM8Ga/nmzhRKya6G0cGw8CAQOjgfwwgfkwHQYDVR0OBBYEFI0cxb6VTEM8YYY6FbBMvAPyT+CyMIHJBgNVHSMEgcEwgb6AFI0cxb6VTEM8YYY6FbBMvAPyT+CyoYGapIGXMIGUMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEQMA4GA1UEChMHQW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDEiMCAGCSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbYIJANWFuGx90071MAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEEBQADggEBABnTDPEF+3iSP0wNfdIjIz1AlnrPzgAIHVvXxunW7SBrDhEglQZBbKJEk5kT0mtKoOD1JMrSu1xuTKEBahWRbqHsXclaXjoBADb0kkjVEJu/Lh5hgYZnOjvlba8Ld7HCKePCVePoTJBdI4fvugnL8TsgK05aIskyY0hKI9L8KfqfGTl1lzOv2KoWD0KWwtAWPoGChZxmQ+nBli+gwYMzM1vAkP+aayLe0a1EQimlOalO762r0GXO0ks+UeXde2Z4e+8S/pf7pITEI/tP+MxJTALw9QUWEv9lKTk+jkbqxbsh8nfBUapfKqYn0eidpwq2AzVp3juYl7//fKnaPhJD9gs=
</item>
</string-array>
<string-array name="com_google_android_gms_fonts_certs_prod" translatable="false">
<item>
MIIEQzCCAyugAwIBAgIJAMLgh0ZkSjCNMA0GCSqGSIb3DQEBBAUAMHQxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRQwEgYDVQQKEwtHb29nbGUgSW5jLjEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDAeFw0wODA4MjEyMzEzMzRaFw0zNjAxMDcyMzEzMzRaMHQxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRQwEgYDVQQKEwtHb29nbGUgSW5jLjEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDCCASAwDQYJKoZIhvcNAQEBBQADggENADCCAQgCggEBAKtWLgDYO6IIrgqWbxJOKdoR8qtW0I9Y4sypEwPpt1TTcvZApxsdyxMJZ2JORland2qSGT2y5b+3JKkedxiLDmpHpDsz2WCbdxgxRczfey5YZnTJ4VZbH0xqWVW/8lGmPav5xVwnIiJS6HXk+BVKZF+JcWjAsb/GEuq/eFdpuzSqeYTcfi6idkyugwfYwXFU1+5fZKUaRKYCwkkFQVfcAs1fXA5V+++FGfvjJ/CxURaSxaBvGdGDhfXE28LWuT9ozCl5xw4Yq5OGazvV24mZVSoOO0yZ31j7kYvtwYK6NeADwbSxDdJEqO4k//0zOHKrUiGYXtqw/A0LFFtqoZKFjnkCAQOjgdkwgdYwHQYDVR0OBBYEFMd9jMIhF1Ylmn/Tgt9r45jk14alMIGmBgNVHSMEgZ4wgZuAFMd9jMIhF1Ylmn/Tgt9r45jk14aloXikdjB0MQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEUMBIGA1UEChMLR29vZ2xlIEluYy4xEDAOBgNVBAsTB0FuZHJvaWQxEDAOBgNVBAMTB0FuZHJvaWSCCQDC4IdGZEowjTAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBAUAA4IBAQBt0lLO74UwLDYKqs6Tm8/yzKkEu116FmH4rkaymUIE0P9KaMftGlMexFlaYjzmB2OxZyl6euNXEsQH8gjwyxCUKRJNexBiGcCEyj6z+a1fuHHvkiaai+KL8W1EyNmgjmyy8AW7P+LLlkR+ho5zEHatRbM/YAnqGcFh5iZBqpknHf1SKMXFh4dd239FJ1jWYfbMDMy3NS5CTMQ2XFI1MvcyUTdZPErjQfTbQe3aDQsQcafEQPD+nqActifKZ0Np0IS9L9kR/wbNvyz6ENwPiTrjV2KRkEjH78ZMcUQXg0L3BYHJ3lc69Vs5Ddf9uUGGMYldX3WfMBEmh/9iFBDAaTCK
</item>
</string-array>
</resources>
<?xml version="1.0" encoding="utf-8"?>
<resources>
<array name="preloaded_fonts" translatable="false">
<item>@font/iceland</item>
</array>
</resources>
...@@ -17,9 +17,9 @@ ...@@ -17,9 +17,9 @@
<string name="port_transproxy">Transproxy port</string> <string name="port_transproxy">Transproxy port</string>
<string name="remote_dns">Remote DNS</string> <string name="remote_dns">Remote DNS</string>
<string name="traffic" translatable="false">%s↑\t%s↓</string>
<string name="stat_summary">Sent: \t\t\t\t\t%3$s\t↑\t%1$s/s\nReceived: \t%4$s\t↓\t%2$s/s</string> <string name="stat_summary">Sent: \t\t\t\t\t%3$s\t↑\t%1$s/s\nReceived: \t%4$s\t↓\t%2$s/s</string>
<string name="stat_profiles" translatable="false">%1$s↑\t%2$s↓</string> <string name="speed">%s/s</string>
<string name="traffic_summary" translatable="false">%1$s/s↑\t%2$s/s↓</string>
<plurals name="bytes"> <plurals name="bytes">
<item quantity="one">Byte</item> <item quantity="one">Byte</item>
<item quantity="other">Bytes</item> <item quantity="other">Bytes</item>
......
/*******************************************************************************/
/* */
/* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* */
/* This program is free software: you can redistribute it and/or modify */
/* it under the terms of the GNU General Public License as published by */
/* the Free Software Foundation, either version 3 of the License, or */
/* (at your option) any later version. */
/* */
/* This program is distributed in the hope that it will be useful, */
/* but WITHOUT ANY WARRANTY; without even the implied warranty of */
/* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the */
/* GNU General Public License for more details. */
/* */
/* You should have received a copy of the GNU General Public License */
/* along with this program. If not, see <http://www.gnu.org/licenses/>. */
/* */
/*******************************************************************************/
package com.github.shadowsocks
import java.util.Locale
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.view.{LayoutInflater, View, ViewGroup}
import android.webkit.{WebView, WebViewClient}
class AboutFragment extends ToolbarFragment {
override def onCreateView(inflater: LayoutInflater, container: ViewGroup, savedInstanceState: Bundle): View =
inflater.inflate(R.layout.layout_about, container, false)
override def onViewCreated(view: View, savedInstanceState: Bundle) {
super.onViewCreated(view, savedInstanceState)
toolbar.setTitle(getString(R.string.about_title).formatLocal(Locale.ENGLISH, BuildConfig.VERSION_NAME))
val web = view.findViewById[WebView](R.id.web_view)
web.loadUrl("file:///android_asset/pages/about.html")
web.setWebViewClient(new WebViewClient() {
override def shouldOverrideUrlLoading(view: WebView, url: String): Boolean = {
getActivity.asInstanceOf[MainActivity].launchUrl(url)
true
}
})
}
}
/*******************************************************************************/
/* */
/* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* */
/* This program is free software: you can redistribute it and/or modify */
/* it under the terms of the GNU General Public License as published by */
/* the Free Software Foundation, either version 3 of the License, or */
/* (at your option) any later version. */
/* */
/* This program is distributed in the hope that it will be useful, */
/* but WITHOUT ANY WARRANTY; without even the implied warranty of */
/* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the */
/* GNU General Public License for more details. */
/* */
/* You should have received a copy of the GNU General Public License */
/* along with this program. If not, see <http://www.gnu.org/licenses/>. */
/* */
/*******************************************************************************/
package com.github.shadowsocks
import java.util.concurrent.atomic.AtomicBoolean
import android.Manifest.permission
import android.animation.{Animator, AnimatorListenerAdapter}
import android.app.TaskStackBuilder
import android.content._
import android.content.pm.PackageManager
import android.graphics.drawable.Drawable
import android.os.{Bundle, Handler}
import android.support.v7.app.AppCompatActivity
import android.support.v7.widget.Toolbar.OnMenuItemClickListener
import android.support.v7.widget.{DefaultItemAnimator, LinearLayoutManager, RecyclerView, Toolbar}
import android.view._
import android.widget._
import com.futuremind.recyclerviewfastscroll.{FastScroller, SectionTitleProvider}
import com.github.shadowsocks.ShadowsocksApplication.app
import com.github.shadowsocks.utils.{Key, Utils}
import scala.collection.JavaConversions._
import scala.collection.mutable
import scala.language.implicitConversions
object AppManager {
case class ProxiedApp(name: String, packageName: String, icon: Drawable)
private case class ListEntry(switch: Switch, text: TextView, icon: ImageView)
private var instance: AppManager = _
private var receiver: BroadcastReceiver = _
private var cachedApps: Array[ProxiedApp] = _
private def getApps(pm: PackageManager) = {
if (receiver == null) receiver = app.listenForPackageChanges {
synchronized(cachedApps = null)
val instance = AppManager.instance
if (instance != null) instance.reloadApps()
}
synchronized {
if (cachedApps == null) cachedApps = pm.getInstalledPackages(PackageManager.GET_PERMISSIONS)
.filter(p => p.requestedPermissions != null && p.requestedPermissions.contains(permission.INTERNET))
.map(p => ProxiedApp(pm.getApplicationLabel(p.applicationInfo).toString, p.packageName,
p.applicationInfo.loadIcon(pm))).toArray
cachedApps
}
}
}
class AppManager extends AppCompatActivity with OnMenuItemClickListener {
import AppManager._
private final class AppViewHolder(val view: View) extends RecyclerView.ViewHolder(view) with View.OnClickListener {
private val icon = itemView.findViewById[ImageView](R.id.itemicon)
private val check = itemView.findViewById[Switch](R.id.itemcheck)
private var item: ProxiedApp = _
itemView.setOnClickListener(this)
private def proxied = proxiedApps.contains(item.packageName)
def bind(app: ProxiedApp) {
this.item = app
icon.setImageDrawable(app.icon)
check.setText(app.name)
check.setChecked(proxied)
}
def onClick(v: View) {
if (proxied) {
proxiedApps.remove(item.packageName)
check.setChecked(false)
} else {
proxiedApps.add(item.packageName)
check.setChecked(true)
}
if (!appsLoading.get) {
app.dataStore.individual = proxiedApps.mkString("\n")
app.dataStore.dirty = true
}
}
}
private final class AppsAdapter extends RecyclerView.Adapter[AppViewHolder] with SectionTitleProvider {
private val apps = getApps(getPackageManager).sortWith((a, b) => {
val aProxied = proxiedApps.contains(a.packageName)
if (aProxied ^ proxiedApps.contains(b.packageName)) aProxied else a.name.compareToIgnoreCase(b.name) < 0
})
def getItemCount: Int = apps.length
def onBindViewHolder(vh: AppViewHolder, i: Int): Unit = vh.bind(apps(i))
def onCreateViewHolder(vg: ViewGroup, i: Int) =
new AppViewHolder(LayoutInflater.from(vg.getContext).inflate(R.layout.layout_apps_item, vg, false))
def getSectionTitle(i: Int): String = apps(i).name.substring(0, 1)
}
private var proxiedApps: mutable.HashSet[String] = _
private var toolbar: Toolbar = _
private var bypassSwitch: Switch = _
private var appListView: RecyclerView = _
private var fastScroller: FastScroller = _
private var loadingView: View = _
private val appsLoading = new AtomicBoolean
private var handler: Handler = _
private def initProxiedApps(str: String = app.dataStore.individual) =
proxiedApps = str.split('\n').to[mutable.HashSet]
override def onDestroy() {
instance = null
super.onDestroy()
if (handler != null) {
handler.removeCallbacksAndMessages(null)
handler = null
}
}
def onMenuItemClick(item: MenuItem): Boolean = {
val clipboard = getSystemService(Context.CLIPBOARD_SERVICE).asInstanceOf[ClipboardManager]
item.getItemId match {
case R.id.action_apply_all =>
app.profileManager.getAllProfiles match {
case Some(profiles) =>
val proxiedAppString = app.dataStore.individual
profiles.foreach(p => {
p.individual = proxiedAppString
app.profileManager.updateProfile(p)
})
Toast.makeText(this, R.string.action_apply_all, Toast.LENGTH_SHORT).show()
case _ => Toast.makeText(this, R.string.action_export_err, Toast.LENGTH_SHORT).show()
}
return true
case R.id.action_export =>
clipboard.setPrimaryClip(ClipData.newPlainText(Key.individual,
app.dataStore.bypass + "\n" + app.dataStore.individual))
Toast.makeText(this, R.string.action_export_msg, Toast.LENGTH_SHORT).show()
return true
case R.id.action_import =>
if (clipboard.hasPrimaryClip) {
val proxiedAppSequence = clipboard.getPrimaryClip.getItemAt(0).getText
if (proxiedAppSequence != null) {
val proxiedAppString = proxiedAppSequence.toString
if (!proxiedAppString.isEmpty) {
val i = proxiedAppString.indexOf('\n')
try {
val (enabled, apps) = if (i < 0) (proxiedAppString, "")
else (proxiedAppString.substring(0, i), proxiedAppString.substring(i + 1))
bypassSwitch.setChecked(enabled.toBoolean)
app.dataStore.individual = apps
app.dataStore.dirty = true
Toast.makeText(this, R.string.action_import_msg, Toast.LENGTH_SHORT).show()
appListView.setVisibility(View.GONE)
fastScroller.setVisibility(View.GONE)
loadingView.setVisibility(View.VISIBLE)
initProxiedApps(apps)
reloadApps()
return true
} catch {
case _: IllegalArgumentException =>
Toast.makeText(this, R.string.action_import_err, Toast.LENGTH_SHORT).show()
}
}
}
}
Toast.makeText(this, R.string.action_import_err, Toast.LENGTH_SHORT).show()
return false
}
false
}
protected override def onCreate(savedInstanceState: Bundle) {
super.onCreate(savedInstanceState)
handler = new Handler()
this.setContentView(R.layout.layout_apps)
toolbar = findViewById(R.id.toolbar).asInstanceOf[Toolbar]
toolbar.setTitle(R.string.proxied_apps)
toolbar.setNavigationIcon(R.drawable.abc_ic_ab_back_material)
toolbar.setNavigationOnClickListener(_ => {
val intent = getParentActivityIntent
if (shouldUpRecreateTask(intent) || isTaskRoot)
TaskStackBuilder.create(this).addNextIntentWithParentStack(intent).startActivities()
else finish()
})
toolbar.inflateMenu(R.menu.app_manager_menu)
toolbar.setOnMenuItemClickListener(this)
if (!app.dataStore.proxyApps) {
app.dataStore.proxyApps = true
app.dataStore.dirty = true
}
findViewById(R.id.onSwitch).asInstanceOf[Switch]
.setOnCheckedChangeListener((_, checked) => {
app.dataStore.proxyApps = checked
app.dataStore.dirty = true
finish()
})
bypassSwitch = findViewById(R.id.bypassSwitch).asInstanceOf[Switch]
bypassSwitch.setChecked(app.dataStore.bypass)
bypassSwitch.setOnCheckedChangeListener((_, checked) => {
app.dataStore.bypass = checked
app.dataStore.dirty = true
})
initProxiedApps()
loadingView = findViewById(R.id.loading)
appListView = findViewById(R.id.list).asInstanceOf[RecyclerView]
appListView.setLayoutManager(new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false))
appListView.setItemAnimator(new DefaultItemAnimator)
fastScroller = findViewById(R.id.fastscroller).asInstanceOf[FastScroller]
instance = this
loadAppsAsync()
}
def reloadApps(): Unit = if (!appsLoading.compareAndSet(true, false)) loadAppsAsync()
def loadAppsAsync() {
if (!appsLoading.compareAndSet(false, true)) return
Utils.ThrowableFuture {
var adapter: AppsAdapter = null
do {
appsLoading.set(true)
adapter = new AppsAdapter
} while (!appsLoading.compareAndSet(true, false))
handler.post(() => {
appListView.setAdapter(adapter)
fastScroller.setRecyclerView(appListView)
val shortAnimTime = getResources.getInteger(android.R.integer.config_shortAnimTime)
appListView.setAlpha(0)
appListView.setVisibility(View.VISIBLE)
appListView.animate().alpha(1).setDuration(shortAnimTime)
fastScroller.setAlpha(0)
fastScroller.setVisibility(View.VISIBLE)
fastScroller.animate().alpha(1).setDuration(shortAnimTime)
loadingView.animate().alpha(0).setDuration(shortAnimTime).setListener(new AnimatorListenerAdapter {
override def onAnimationEnd(animation: Animator): Unit = loadingView.setVisibility(View.GONE)
})
})
}
}
override def onKeyUp(keyCode: Int, event: KeyEvent): Boolean = keyCode match {
case KeyEvent.KEYCODE_MENU =>
if (toolbar.isOverflowMenuShowing) toolbar.hideOverflowMenu else toolbar.showOverflowMenu
case _ => super.onKeyUp(keyCode, event)
}
}
/*******************************************************************************/
/* */
/* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* */
/* This program is free software: you can redistribute it and/or modify */
/* it under the terms of the GNU General Public License as published by */
/* the Free Software Foundation, either version 3 of the License, or */
/* (at your option) any later version. */
/* */
/* This program is distributed in the hope that it will be useful, */
/* but WITHOUT ANY WARRANTY; without even the implied warranty of */
/* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the */
/* GNU General Public License for more details. */
/* */
/* You should have received a copy of the GNU General Public License */
/* along with this program. If not, see <http://www.gnu.org/licenses/>. */
/* */
/*******************************************************************************/
package com.github.shadowsocks
import android.content.pm.PackageManager
import android.content.{BroadcastReceiver, ComponentName, Context, Intent}
import com.github.shadowsocks.utils._
object BootReceiver {
def getEnabled(context: Context): Boolean = PackageManager.COMPONENT_ENABLED_STATE_ENABLED ==
context.getPackageManager.getComponentEnabledSetting(new ComponentName(context, classOf[BootReceiver]))
def setEnabled(context: Context, enabled: Boolean): Unit = context.getPackageManager.setComponentEnabledSetting(
new ComponentName(context, classOf[BootReceiver]),
if (enabled) PackageManager.COMPONENT_ENABLED_STATE_ENABLED else PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
PackageManager.DONT_KILL_APP)
}
class BootReceiver extends BroadcastReceiver {
def onReceive(context: Context, intent: Intent): Unit = {
Utils.startSsService(context)
}
}
/*******************************************************************************/
/* */
/* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* */
/* This program is free software: you can redistribute it and/or modify */
/* it under the terms of the GNU General Public License as published by */
/* the Free Software Foundation, either version 3 of the License, or */
/* (at your option) any later version. */
/* */
/* This program is distributed in the hope that it will be useful, */
/* but WITHOUT ANY WARRANTY; without even the implied warranty of */
/* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the */
/* GNU General Public License for more details. */
/* */
/* You should have received a copy of the GNU General Public License */
/* along with this program. If not, see <http://www.gnu.org/licenses/>. */
/* */
/*******************************************************************************/
package com.github.shadowsocks
import android.os.Bundle
import android.support.design.widget.Snackbar
import android.support.v14.preference.SwitchPreference
import android.support.v7.preference.Preference
import com.github.shadowsocks.ShadowsocksApplication.app
import com.github.shadowsocks.bg.ServiceState
import com.github.shadowsocks.utils.{Key, TcpFastOpen}
import com.takisoft.fix.support.v7.preference.PreferenceFragmentCompatDividers
class GlobalConfigFragment extends PreferenceFragmentCompatDividers {
override def onCreatePreferencesFix(bundle: Bundle, key: String) {
getPreferenceManager.setPreferenceDataStore(app.dataStore)
app.dataStore.initGlobal()
addPreferencesFromResource(R.xml.pref_global)
val switch = findPreference(Key.isAutoConnect).asInstanceOf[SwitchPreference]
switch.setOnPreferenceChangeListener((_, value) => {
BootReceiver.setEnabled(getActivity, value.asInstanceOf[Boolean])
true
})
switch.setChecked(BootReceiver.getEnabled(getActivity))
val tfo = findPreference(Key.tfo).asInstanceOf[SwitchPreference]
tfo.setChecked(TcpFastOpen.sendEnabled)
tfo.setOnPreferenceChangeListener((_, v) => {
val value = v.asInstanceOf[Boolean]
val result = TcpFastOpen.enabled(value)
if (result != null && result != "Success.")
Snackbar.make(getActivity.findViewById(R.id.snackbar), result, Snackbar.LENGTH_LONG).show()
value == TcpFastOpen.sendEnabled
})
if (!TcpFastOpen.supported) {
tfo.setEnabled(false)
tfo.setSummary(getString(R.string.tcp_fastopen_summary_unsupported, java.lang.System.getProperty("os.version")))
}
val serviceMode = findPreference(Key.serviceMode)
val portProxy = findPreference(Key.portProxy)
val portLocalDns = findPreference(Key.portLocalDns)
val portTransproxy = findPreference(Key.portTransproxy)
def onServiceModeChange(p: Preference, v: Any) = {
val (enabledLocalDns, enabledTransproxy) = v match {
case Key.modeProxy => (false, false)
case Key.modeVpn => (true, false)
case Key.modeTransproxy => (true, true)
}
portLocalDns.setEnabled(enabledLocalDns)
portTransproxy.setEnabled(enabledTransproxy)
true
}
MainActivity.stateListener = {
case ServiceState.IDLE | ServiceState.STOPPED =>
serviceMode.setEnabled(true)
portProxy.setEnabled(true)
onServiceModeChange(null, app.dataStore.serviceMode)
case _ =>
serviceMode.setEnabled(false)
portProxy.setEnabled(false)
portLocalDns.setEnabled(false)
portTransproxy.setEnabled(false)
}
MainActivity.stateListener(getActivity.asInstanceOf[MainActivity].state)
serviceMode.setOnPreferenceChangeListener(onServiceModeChange)
}
override def onDestroy() {
MainActivity.stateListener = null
super.onDestroy()
}
}
/*******************************************************************************/
/* */
/* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* */
/* This program is free software: you can redistribute it and/or modify */
/* it under the terms of the GNU General Public License as published by */
/* the Free Software Foundation, either version 3 of the License, or */
/* (at your option) any later version. */
/* */
/* This program is distributed in the hope that it will be useful, */
/* but WITHOUT ANY WARRANTY; without even the implied warranty of */
/* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the */
/* GNU General Public License for more details. */
/* */
/* You should have received a copy of the GNU General Public License */
/* along with this program. If not, see <http://www.gnu.org/licenses/>. */
/* */
/*******************************************************************************/
package com.github.shadowsocks
import android.os.Bundle
import android.view.{LayoutInflater, View, ViewGroup}
import android.app.Fragment
class GlobalSettingsFragment extends ToolbarFragment {
override def onCreateView(inflater: LayoutInflater, container: ViewGroup, savedInstanceState: Bundle): View =
inflater.inflate(R.layout.layout_global_settings, container, false)
override def onViewCreated(view: View, savedInstanceState: Bundle) {
super.onViewCreated(view, savedInstanceState)
toolbar.setTitle(R.string.settings)
val fm = getChildFragmentManager
fm.beginTransaction().replace(R.id.content, new GlobalConfigFragment()).commit()
fm.executePendingTransactions()
}
override def onDetach() {
super.onDetach()
try {
val childFragmentManager = classOf[Fragment].getDeclaredField("mChildFragmentManager")
childFragmentManager.setAccessible(true)
childFragmentManager.set(this, null)
} catch {
case _: Exception => // ignore
}
}
}
/*******************************************************************************/
/* */
/* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* */
/* This program is free software: you can redistribute it and/or modify */
/* it under the terms of the GNU General Public License as published by */
/* the Free Software Foundation, either version 3 of the License, or */
/* (at your option) any later version. */
/* */
/* This program is distributed in the hope that it will be useful, */
/* but WITHOUT ANY WARRANTY; without even the implied warranty of */
/* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the */
/* GNU General Public License for more details. */
/* */
/* You should have received a copy of the GNU General Public License */
/* along with this program. If not, see <http://www.gnu.org/licenses/>. */
/* */
/*******************************************************************************/
package com.github.shadowsocks
import java.io._
import java.lang.System.currentTimeMillis
import java.util.concurrent.Semaphore
import android.os.Build
import android.util.Log
import com.github.shadowsocks.utils.CloseUtils._
import com.github.shadowsocks.utils.Commandline
import com.github.shadowsocks.ShadowsocksApplication.app
import scala.collection.JavaConversions._
import scala.collection.immutable.Stream
class StreamLogger(is: InputStream, tag: String, logger: (String, String) => Int) extends Thread {
override def run(): Unit = autoClose(new BufferedReader(new InputStreamReader(is)))(br =>
try Stream.continually(br.readLine()).takeWhile(_ != null).foreach(logger(tag, _)) catch {
case _: IOException =>
})
}
/**
* @author ayanamist@gmail.com
*/
class GuardedProcess(cmd: String*) {
private val TAG = classOf[GuardedProcess].getSimpleName
@volatile private var guardThread: Thread = _
@volatile private var isDestroyed: Boolean = _
@volatile private var process: Process = _
@volatile private var isRestart = false
def start(onRestartCallback: () => Unit = null): GuardedProcess = {
val semaphore = new Semaphore(1)
semaphore.acquire()
@volatile var ioException: IOException = null
guardThread = new Thread(() => {
try {
var callback: () => Unit = null
while (!isDestroyed) {
if (BuildConfig.DEBUG) Log.d(TAG, "start process: " + Commandline.toString(cmd))
val startTime = currentTimeMillis
process = new ProcessBuilder(cmd)
.redirectErrorStream(true)
.directory(app.getFilesDir)
.start()
new StreamLogger(process.getInputStream(), TAG, Log.i).start()
new StreamLogger(process.getErrorStream(), TAG, Log.e).start()
if (callback == null) callback = onRestartCallback else callback()
semaphore.release()
process.waitFor
this.synchronized {
if (isRestart) {
isRestart = false
} else {
if (currentTimeMillis - startTime < 1000) {
Log.w(TAG, "process exit too fast, stop guard: " + Commandline.toString(cmd))
isDestroyed = true
}
}
}
}
} catch {
case _: InterruptedException =>
if (BuildConfig.DEBUG) Log.d(TAG, "thread interrupt, destroy process: " + Commandline.toString(cmd))
destroyProcess()
case e: IOException => ioException = e
} finally semaphore.release()
}, "GuardThread-" + cmd.head)
guardThread.start()
semaphore.acquire()
if (ioException != null) throw ioException
this
}
def destroy() {
isDestroyed = true
guardThread.interrupt()
destroyProcess()
try guardThread.join() catch {
case _: InterruptedException =>
}
}
private def destroyProcess() {
if (Build.VERSION.SDK_INT < 24) {
JniHelper.sigtermCompat(process)
JniHelper.waitForCompat(process, 500)
}
process.destroy()
}
def restart() {
this.synchronized {
isRestart = true
destroyProcess()
}
}
@throws(classOf[InterruptedException])
def waitFor: Int = {
guardThread.join()
0
}
}
/*******************************************************************************/
/* */
/* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* */
/* This program is free software: you can redistribute it and/or modify */
/* it under the terms of the GNU General Public License as published by */
/* the Free Software Foundation, either version 3 of the License, or */
/* (at your option) any later version. */
/* */
/* This program is distributed in the hope that it will be useful, */
/* but WITHOUT ANY WARRANTY; without even the implied warranty of */
/* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the */
/* GNU General Public License for more details. */
/* */
/* You should have received a copy of the GNU General Public License */
/* along with this program. If not, see <http://www.gnu.org/licenses/>. */
/* */
/*******************************************************************************/
package com.github.shadowsocks
import java.lang.System.currentTimeMillis
import java.net.{HttpURLConnection, InetSocketAddress, URL, Proxy => JavaProxy}
import java.util.Locale
import android.app.Activity
import android.app.backup.BackupManager
import android.content._
import android.net.{Uri, VpnService}
import android.nfc.{NdefMessage, NfcAdapter}
import android.os.{Bundle, Handler}
import android.support.customtabs.CustomTabsIntent
import android.support.design.widget.{FloatingActionButton, Snackbar}
import android.support.v4.content.ContextCompat
import android.support.v7.app.{AlertDialog, AppCompatActivity}
import android.support.v7.content.res.AppCompatResources
import android.support.v7.preference.PreferenceDataStore
import android.support.v7.widget.RecyclerView.ViewHolder
import android.text.TextUtils
import android.util.Log
import android.view.View
import android.widget.{TextView, Toast}
import com.github.jorgecastilloprz.FABProgressCircle
import com.github.shadowsocks.ShadowsocksApplication.app
import com.github.shadowsocks.acl.{Acl, CustomRulesFragment}
import com.github.shadowsocks.aidl.IShadowsocksServiceCallback
import com.github.shadowsocks.bg.{Executable, ServiceState, TrafficMonitor}
import com.github.shadowsocks.preference.OnPreferenceDataStoreChangeListener
import com.github.shadowsocks.utils.CloseUtils.autoDisconnect
import com.github.shadowsocks.utils._
import com.mikepenz.crossfader.Crossfader
import com.mikepenz.crossfader.view.CrossFadeSlidingPaneLayout
import com.mikepenz.materialdrawer.interfaces.ICrossfader
import com.mikepenz.materialdrawer.model.PrimaryDrawerItem
import com.mikepenz.materialdrawer.model.interfaces.IDrawerItem
import com.mikepenz.materialdrawer.{Drawer, DrawerBuilder}
object MainActivity {
private final val TAG = "ShadowsocksMainActivity"
private final val REQUEST_CONNECT = 1
private final val DRAWER_PROFILES = 0L
private final val DRAWER_GLOBAL_SETTINGS = 1L
private final val DRAWER_ABOUT = 3L
private final val DRAWER_FAQ = 4L
private final val DRAWER_CUSTOM_RULES = 5L
var stateListener: Int => Unit = _
}
class MainActivity extends AppCompatActivity with ServiceBoundContext with Drawer.OnDrawerItemClickListener
with OnPreferenceDataStoreChangeListener {
import MainActivity._
// UI
private val handler = new Handler()
private var fab: FloatingActionButton = _
private var fabProgressCircle: FABProgressCircle = _
var crossfader: Crossfader[CrossFadeSlidingPaneLayout] = _
var drawer: Drawer = _
private var testCount: Int = _
private var statusText: TextView = _
private var txText: TextView = _
private var rxText: TextView = _
private var txRateText: TextView = _
private var rxRateText: TextView = _
private lazy val customTabsIntent = new CustomTabsIntent.Builder()
.setToolbarColor(ContextCompat.getColor(this, R.color.material_primary_500))
.build()
def launchUrl(url: String): Unit = try customTabsIntent.launchUrl(this, Uri.parse(url)) catch {
case _: ActivityNotFoundException => // Ignore
}
// Services
var state: Int = _
private val callback = new IShadowsocksServiceCallback.Stub {
def stateChanged(s: Int, profileName: String, m: String): Unit = handler.post(() => changeState(s, profileName, m))
def trafficUpdated(profileId: Int, txRate: Long, rxRate: Long, txTotal: Long, rxTotal: Long): Unit =
handler.post(() => updateTraffic(profileId, txRate, rxRate, txTotal, rxTotal))
override def trafficPersisted(profileId: Int): Unit = handler.post(() => if (ProfilesFragment.instance != null)
ProfilesFragment.instance.onTrafficPersisted(profileId))
}
private lazy val greyTint = ContextCompat.getColorStateList(MainActivity.this, R.color.material_primary_500)
private lazy val greenTint = ContextCompat.getColorStateList(MainActivity.this, R.color.material_green_700)
private def hideCircle() = try fabProgressCircle.hide() catch {
case _: NullPointerException =>
}
private def changeState(s: Int, profileName: String = null, m: String = null) {
s match {
case ServiceState.CONNECTING =>
fab.setImageResource(R.drawable.ic_start_busy)
fabProgressCircle.show()
statusText.setText(R.string.connecting)
case ServiceState.CONNECTED =>
if (state == ServiceState.CONNECTING) fabProgressCircle.beginFinalAnimation()
else fabProgressCircle.postDelayed(hideCircle, 1000)
fab.setImageResource(R.drawable.ic_start_connected)
statusText.setText(R.string.vpn_connected)
case ServiceState.STOPPING =>
fab.setImageResource(R.drawable.ic_start_busy)
if (state == ServiceState.CONNECTED) fabProgressCircle.show() // ignore for stopped
statusText.setText(R.string.stopping)
case _ =>
fab.setImageResource(R.drawable.ic_start_idle)
fabProgressCircle.postDelayed(hideCircle, 1000)
if (m != null) {
Snackbar.make(findViewById(R.id.snackbar),
getString(R.string.vpn_error).formatLocal(Locale.ENGLISH, m), Snackbar.LENGTH_LONG).show()
Log.e(TAG, "Error to start VPN service: " + m)
}
statusText.setText(R.string.not_connected)
}
state = s
if (state == ServiceState.CONNECTED) fab.setBackgroundTintList(greenTint) else {
fab.setBackgroundTintList(greyTint)
updateTraffic(-1, 0, 0, 0, 0)
testCount += 1 // suppress previous test messages
}
if (ProfilesFragment.instance != null)
ProfilesFragment.instance.profilesAdapter.notifyDataSetChanged() // refresh button enabled state
if (stateListener != null) stateListener(s)
fab.setEnabled(false)
if (state == ServiceState.CONNECTED || state == ServiceState.STOPPED)
handler.postDelayed(() => fab.setEnabled(state == ServiceState.CONNECTED || state == ServiceState.STOPPED), 1000)
}
def updateTraffic(profileId: Int, txRate: Long, rxRate: Long, txTotal: Long, rxTotal: Long) {
txText.setText(TrafficMonitor.formatTraffic(txTotal))
rxText.setText(TrafficMonitor.formatTraffic(rxTotal))
txRateText.setText(TrafficMonitor.formatTraffic(txRate) + "/s")
rxRateText.setText(TrafficMonitor.formatTraffic(rxRate) + "/s")
val child = getSupportFragmentManager.findFragmentById(R.id.fragment_holder).asInstanceOf[ToolbarFragment]
if (state != ServiceState.STOPPING && child != null)
child.onTrafficUpdated(profileId, txRate, rxRate, txTotal, rxTotal)
}
override def onServiceConnected(): Unit = changeState(bgService.getState)
override def onServiceDisconnected(): Unit = changeState(ServiceState.IDLE)
override def binderDied(): Unit = handler.post(() => {
detachService()
Executable.killAll()
attachService(callback)
})
override def onActivityResult(requestCode: Int, resultCode: Int, data: Intent): Unit = resultCode match {
case Activity.RESULT_OK => Utils.startSsService(this)
case _ =>
Snackbar.make(findViewById(R.id.snackbar), R.string.vpn_permission_denied, Snackbar.LENGTH_LONG).show()
Log.e(TAG, "Failed to start VpnService: %s".formatLocal(Locale.ENGLISH, data))
}
override def onCreate(savedInstanceState: Bundle) {
super.onCreate(savedInstanceState)
setContentView(R.layout.layout_main)
val drawerBuilder = new DrawerBuilder()
.withActivity(this)
.withTranslucentStatusBar(true)
.withHeader(R.layout.layout_header)
.addDrawerItems(
new PrimaryDrawerItem()
.withIdentifier(DRAWER_PROFILES)
.withName(R.string.profiles)
.withIcon(AppCompatResources.getDrawable(this, R.drawable.ic_action_description))
.withIconTintingEnabled(true),
new PrimaryDrawerItem()
.withIdentifier(DRAWER_CUSTOM_RULES)
.withName(R.string.custom_rules)
.withIcon(AppCompatResources.getDrawable(this, R.drawable.ic_action_assignment))
.withIconTintingEnabled(true),
new PrimaryDrawerItem()
.withIdentifier(DRAWER_GLOBAL_SETTINGS)
.withName(R.string.settings)
.withIcon(AppCompatResources.getDrawable(this, R.drawable.ic_action_settings))
.withIconTintingEnabled(true)
)
.addStickyDrawerItems(
new PrimaryDrawerItem()
.withIdentifier(DRAWER_FAQ)
.withName(R.string.faq)
.withIcon(AppCompatResources.getDrawable(this, R.drawable.ic_action_help_outline))
.withIconTintingEnabled(true)
.withSelectable(false),
new PrimaryDrawerItem()
.withIdentifier(DRAWER_ABOUT)
.withName(R.string.about)
.withIcon(AppCompatResources.getDrawable(this, R.drawable.ic_action_copyright))
.withIconTintingEnabled(true)
)
.withOnDrawerItemClickListener(this)
.withActionBarDrawerToggle(true)
.withSavedInstance(savedInstanceState)
val miniDrawerWidth = getResources.getDimension(R.dimen.material_mini_drawer_item)
if (getResources.getDisplayMetrics.widthPixels >=
getResources.getDimension(R.dimen.profile_item_max_width) + miniDrawerWidth) {
drawer = drawerBuilder.withGenerateMiniDrawer(true).buildView()
crossfader = new Crossfader[CrossFadeSlidingPaneLayout]()
crossfader.withContent(findViewById(android.R.id.content))
.withFirst(drawer.getSlider, getResources.getDimensionPixelSize(R.dimen.material_drawer_width))
.withSecond(drawer.getMiniDrawer.build(this), miniDrawerWidth.toInt)
.withSavedInstance(savedInstanceState)
.build()
if (getResources.getConfiguration.getLayoutDirection == View.LAYOUT_DIRECTION_RTL)
crossfader.getCrossFadeSlidingPaneLayout.setShadowDrawableRight(
AppCompatResources.getDrawable(this, R.drawable.material_drawer_shadow_right))
else crossfader.getCrossFadeSlidingPaneLayout.setShadowDrawableLeft(
AppCompatResources.getDrawable(this, R.drawable.material_drawer_shadow_left))
drawer.getMiniDrawer.withCrossFader(new ICrossfader { // a wrapper is needed
def isCrossfaded: Boolean = crossfader.isCrossFaded
def crossfade(): Unit = crossfader.crossFade()
})
} else drawer = drawerBuilder.build()
val header = drawer.getHeader
val title = header.findViewById[TextView](R.id.drawer_title)
val tf = Typefaces.get(this, "fonts/Iceland.ttf")
if (tf != null) title.setTypeface(tf)
if (savedInstanceState == null) displayFragment(new ProfilesFragment)
previousPosition = drawer.getCurrentSelectedPosition
statusText = findViewById(R.id.status).asInstanceOf[TextView]
txText = findViewById(R.id.tx).asInstanceOf[TextView]
txRateText = findViewById(R.id.txRate).asInstanceOf[TextView]
rxText = findViewById(R.id.rx).asInstanceOf[TextView]
rxRateText = findViewById(R.id.rxRate).asInstanceOf[TextView]
findViewById[View](R.id.stat).setOnClickListener(_ => if (state == ServiceState.CONNECTED) {
testCount += 1
statusText.setText(R.string.connection_test_testing)
val id = testCount // it would change by other code
Utils.ThrowableFuture {
// Based on: https://android.googlesource.com/platform/frameworks/base/+/master/services/core/java/com/android/server/connectivity/NetworkMonitor.java#640
autoDisconnect {
val url = new URL("https", app.currentProfile.get.route match {
case Acl.CHINALIST => "www.qualcomm.cn"
case _ => "www.google.com"
}, "/generate_204")
(if (app.usingVpnMode) url.openConnection() else url.openConnection(
new JavaProxy(JavaProxy.Type.SOCKS, new InetSocketAddress("127.0.0.1", app.dataStore.portProxy))))
.asInstanceOf[HttpURLConnection]
} { conn =>
conn.setConnectTimeout(5 * 1000)
conn.setReadTimeout(5 * 1000)
conn.setInstanceFollowRedirects(false)
conn.setUseCaches(false)
if (testCount == id) {
var result: String = null
var success = true
try {
val start = currentTimeMillis
conn.getInputStream
val elapsed = currentTimeMillis - start
val code = conn.getResponseCode
if (code == 204 || code == 200 && conn.getContentLength == 0)
result = getString(R.string.connection_test_available, elapsed: java.lang.Long)
else throw new Exception(getString(R.string.connection_test_error_status_code, code: Integer))
} catch {
case e: Exception =>
success = false
result = getString(R.string.connection_test_error, e.getMessage)
}
if (testCount == id) handler.post(() => if (success) statusText.setText(result) else {
statusText.setText(R.string.connection_test_fail)
Snackbar.make(findViewById(R.id.snackbar), result, Snackbar.LENGTH_LONG).show()
})
}
}
}
})
fab = findViewById(R.id.fab).asInstanceOf[FloatingActionButton]
fabProgressCircle = findViewById(R.id.fabProgressCircle).asInstanceOf[FABProgressCircle]
fab.setOnClickListener(_ => if (state == ServiceState.CONNECTED) Utils.stopSsService(this) else Utils.ThrowableFuture {
if (app.usingVpnMode) {
val intent = VpnService.prepare(this)
if (intent != null) startActivityForResult(intent, REQUEST_CONNECT)
else handler.post(() => onActivityResult(REQUEST_CONNECT, Activity.RESULT_OK, null))
} else Utils.startSsService(this)
})
fab.setOnLongClickListener(_ => {
Utils.positionToast(Toast.makeText(this, if (state == ServiceState.CONNECTED) R.string.stop else R.string.connect,
Toast.LENGTH_SHORT), fab, getWindow, 0, getResources.getDimensionPixelOffset(R.dimen.margin_small)).show()
true
})
changeState(ServiceState.IDLE) // reset everything to init state
handler.post(() => attachService(callback))
app.dataStore.registerChangeListener(this)
val intent = getIntent
if (intent != null) handleShareIntent(intent)
}
override def onNewIntent(intent: Intent) {
super.onNewIntent(intent)
handleShareIntent(intent)
}
def handleShareIntent(intent: Intent) {
val sharedStr = intent.getAction match {
case Intent.ACTION_VIEW => intent.getData.toString
case NfcAdapter.ACTION_NDEF_DISCOVERED =>
val rawMsgs = intent.getParcelableArrayExtra(NfcAdapter.EXTRA_NDEF_MESSAGES)
if (rawMsgs != null && rawMsgs.nonEmpty)
new String(rawMsgs(0).asInstanceOf[NdefMessage].getRecords()(0).getPayload)
else null
case _ => null
}
if (TextUtils.isEmpty(sharedStr)) return
val profiles = Parser.findAll(sharedStr).toList
if (profiles.isEmpty) {
Snackbar.make(findViewById(R.id.snackbar), R.string.profile_invalid_input, Snackbar.LENGTH_LONG).show()
return
}
new AlertDialog.Builder(this)
.setTitle(R.string.add_profile_dialog)
.setPositiveButton(R.string.yes, ((_, _) =>
profiles.foreach(app.profileManager.createProfile)): DialogInterface.OnClickListener)
.setNegativeButton(R.string.no, null)
.setMessage(profiles.mkString("\n"))
.create()
.show()
}
def onPreferenceDataStoreChanged(store: PreferenceDataStore, key: String): Unit = key match {
case Key.serviceMode => handler.post(() => {
detachService()
attachService(callback)
})
case _ =>
}
private def displayFragment(fragment: ToolbarFragment) {
getSupportFragmentManager.beginTransaction().replace(R.id.fragment_holder, fragment).commitAllowingStateLoss()
drawer.closeDrawer()
}
private var previousPosition: Int = _
override def onItemClick(view: View, position: Int, drawerItem: IDrawerItem[_, _ <: ViewHolder]): Boolean = {
if (position == previousPosition) drawer.closeDrawer() else drawerItem.getIdentifier match {
case DRAWER_PROFILES => displayFragment(new ProfilesFragment)
case DRAWER_GLOBAL_SETTINGS => displayFragment(new GlobalSettingsFragment)
case DRAWER_ABOUT =>
app.track(TAG, "about")
displayFragment(new AboutFragment)
case DRAWER_FAQ => launchUrl(getString(R.string.faq_url))
case DRAWER_CUSTOM_RULES => displayFragment(new CustomRulesFragment)
case _ => // Ignore
}
previousPosition = position
true // unexpected cases will throw exception
}
protected override def onResume() {
super.onResume()
app.remoteConfig.fetch()
state match {
case ServiceState.STOPPING | ServiceState.CONNECTING =>
case _ => hideCircle()
}
}
override def onStart() {
super.onStart()
setListeningForBandwidth(true)
}
override def onBackPressed(): Unit = if (drawer.isDrawerOpen) drawer.closeDrawer() else {
val currentFragment = getSupportFragmentManager.findFragmentById(R.id.fragment_holder).asInstanceOf[ToolbarFragment]
if (!currentFragment.onBackPressed())
if (currentFragment.isInstanceOf[ProfilesFragment]) super.onBackPressed()
else drawer.setSelection(DRAWER_PROFILES)
}
override def onStop() {
setListeningForBandwidth(false)
super.onStop()
}
override def onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
drawer.saveInstanceState(outState)
if (crossfader != null) crossfader.saveInstanceState(outState)
}
override def onDestroy() {
super.onDestroy()
app.dataStore.unregisterChangeListener(this)
detachService()
new BackupManager(this).dataChanged()
handler.removeCallbacksAndMessages(null)
}
}
/*******************************************************************************/
/* */
/* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* */
/* This program is free software: you can redistribute it and/or modify */
/* it under the terms of the GNU General Public License as published by */
/* the Free Software Foundation, either version 3 of the License, or */
/* (at your option) any later version. */
/* */
/* This program is distributed in the hope that it will be useful, */
/* but WITHOUT ANY WARRANTY; without even the implied warranty of */
/* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the */
/* GNU General Public License for more details. */
/* */
/* You should have received a copy of the GNU General Public License */
/* along with this program. If not, see <http://www.gnu.org/licenses/>. */
/* */
/*******************************************************************************/
package com.github.shadowsocks
import android.app.Activity
import android.content.{DialogInterface, Intent}
import android.os.Bundle
import android.support.v7.app.{AlertDialog, AppCompatActivity}
import android.support.v7.widget.Toolbar
import com.github.shadowsocks.ShadowsocksApplication.app
import com.github.shadowsocks.plugin.PluginContract
object ProfileConfigActivity {
final val REQUEST_CODE_PLUGIN_HELP = 1
}
class ProfileConfigActivity extends AppCompatActivity {
import ProfileConfigActivity._
private lazy val child = getSupportFragmentManager.findFragmentById(R.id.content).asInstanceOf[ProfileConfigFragment]
override def onCreate(savedInstanceState: Bundle) {
super.onCreate(savedInstanceState)
setContentView(R.layout.layout_profile_config)
val toolbar = findViewById(R.id.toolbar).asInstanceOf[Toolbar]
toolbar.setTitle(R.string.profile_config)
toolbar.setNavigationIcon(R.drawable.ic_navigation_close)
toolbar.setNavigationOnClickListener(_ => onBackPressed())
toolbar.inflateMenu(R.menu.profile_config_menu)
toolbar.setOnMenuItemClickListener(child)
}
override def onBackPressed(): Unit = if (app.dataStore.dirty) new AlertDialog.Builder(this)
.setTitle(R.string.unsaved_changes_prompt)
.setPositiveButton(R.string.yes, ((_, _) => child.saveAndExit()): DialogInterface.OnClickListener)
.setNegativeButton(R.string.no, ((_, _) => finish()): DialogInterface.OnClickListener)
.setNeutralButton(android.R.string.cancel, null)
.create()
.show() else super.onBackPressed()
override def onActivityResult(requestCode: Int, resultCode: Int, data: Intent): Unit = requestCode match {
case REQUEST_CODE_PLUGIN_HELP => if (resultCode == Activity.RESULT_OK) new AlertDialog.Builder(this)
.setTitle("?")
.setMessage(data.getCharSequenceExtra(PluginContract.EXTRA_HELP_MESSAGE))
.show()
case _ => super.onActivityResult(requestCode, resultCode, data)
}
}
/*******************************************************************************/
/* */
/* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* */
/* This program is free software: you can redistribute it and/or modify */
/* it under the terms of the GNU General Public License as published by */
/* the Free Software Foundation, either version 3 of the License, or */
/* (at your option) any later version. */
/* */
/* This program is distributed in the hope that it will be useful, */
/* but WITHOUT ANY WARRANTY; without even the implied warranty of */
/* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the */
/* GNU General Public License for more details. */
/* */
/* You should have received a copy of the GNU General Public License */
/* along with this program. If not, see <http://www.gnu.org/licenses/>. */
/* */
/*******************************************************************************/
package com.github.shadowsocks
import android.app.Activity
import android.content._
import android.os.{Build, Bundle, UserManager}
import android.support.design.widget.Snackbar
import android.support.v14.preference.SwitchPreference
import android.support.v7.app.AlertDialog
import android.support.v7.preference.{Preference, PreferenceDataStore}
import android.support.v7.widget.Toolbar.OnMenuItemClickListener
import android.text.TextUtils
import android.view.MenuItem
import com.github.shadowsocks.ShadowsocksApplication.app
import com.github.shadowsocks.database.Profile
import com.github.shadowsocks.plugin._
import com.github.shadowsocks.preference.{IconListPreference, OnPreferenceDataStoreChangeListener, PluginConfigurationDialogFragment}
import com.github.shadowsocks.utils.{Action, Key}
import com.takisoft.fix.support.v7.preference.{EditTextPreference, PreferenceFragmentCompatDividers}
object ProfileConfigFragment {
private final val REQUEST_CODE_PLUGIN_CONFIGURE = 1
private final val FRAGMENT_DIALOG_TAG = "android.support.v7.preference.PreferenceFragment.DIALOG"
}
class ProfileConfigFragment extends PreferenceFragmentCompatDividers with OnMenuItemClickListener
with OnPreferenceDataStoreChangeListener {
import ProfileConfigFragment._
private var profile: Profile = _
private var isProxyApps: SwitchPreference = _
private var plugin: IconListPreference = _
private var pluginConfigure: EditTextPreference = _
private var pluginConfiguration: PluginConfiguration = _
override def onCreatePreferencesFix(bundle: Bundle, key: String) {
getPreferenceManager.setPreferenceDataStore(app.dataStore)
app.profileManager.getProfile(getActivity.getIntent.getIntExtra(Action.EXTRA_PROFILE_ID, -1)) match {
case Some(p) =>
profile = p
profile.serialize(app.dataStore)
case None => getActivity.finish()
}
addPreferencesFromResource(R.xml.pref_profile)
if (Build.VERSION.SDK_INT >= 25 && getActivity.getSystemService(classOf[UserManager]).isDemoUser) {
findPreference(Key.host).setSummary("shadowsocks.example.org")
findPreference(Key.remotePort).setSummary("1337")
findPreference(Key.password).setSummary("\u2022" * 32)
}
isProxyApps = findPreference(Key.proxyApps).asInstanceOf[SwitchPreference]
if (Build.VERSION.SDK_INT < 21) isProxyApps.getParent.removePreference(isProxyApps) else {
isProxyApps.setEnabled(app.usingVpnMode)
isProxyApps.setOnPreferenceClickListener(_ => {
startActivity(new Intent(getActivity, classOf[AppManager]))
isProxyApps.setChecked(true)
false
})
}
plugin = findPreference(Key.plugin).asInstanceOf[IconListPreference]
pluginConfigure = findPreference(Key.pluginConfigure).asInstanceOf[EditTextPreference]
plugin.unknownValueSummary = getString(R.string.plugin_unknown)
plugin.setOnPreferenceChangeListener((_, value) => {
val selected = value.asInstanceOf[String]
pluginConfiguration = new PluginConfiguration(pluginConfiguration.pluginsOptions, selected)
app.dataStore.plugin = pluginConfiguration.toString
app.dataStore.dirty = true
pluginConfigure.setEnabled(!TextUtils.isEmpty(selected))
pluginConfigure.setText(pluginConfiguration.selectedOptions.toString)
if (!PluginManager.fetchPlugins()(selected).trusted)
Snackbar.make(getView, R.string.plugin_untrusted, Snackbar.LENGTH_LONG).show()
true
})
pluginConfigure.setOnPreferenceChangeListener(onPluginConfigureChanged)
initPlugins()
app.listenForPackageChanges(initPlugins())
app.dataStore.registerChangeListener(this)
}
def initPlugins() {
val plugins = PluginManager.fetchPlugins()
plugin.setEntries(plugins.map(_._2.label).toArray)
plugin.setEntryValues(plugins.map(_._2.id.asInstanceOf[CharSequence]).toArray)
plugin.setEntryIcons(plugins.map(_._2.icon).toArray)
plugin.entryPackageNames = plugins.map(_._2.packageName).toArray
pluginConfiguration = new PluginConfiguration(app.dataStore.plugin)
plugin.setValue(pluginConfiguration.selected)
plugin.init()
plugin.checkSummary()
pluginConfigure.setEnabled(!TextUtils.isEmpty(pluginConfiguration.selected))
pluginConfigure.setText(pluginConfiguration.selectedOptions.toString)
}
private def onPluginConfigureChanged(p: Preference, value: Any) = try {
val selected = pluginConfiguration.selected
pluginConfiguration = new PluginConfiguration(pluginConfiguration.pluginsOptions +
(pluginConfiguration.selected -> new PluginOptions(selected, value.asInstanceOf[String])), selected)
app.dataStore.plugin = pluginConfiguration.toString
app.dataStore.dirty = true
true
} catch {
case exc: IllegalArgumentException =>
Snackbar.make(getView, exc.getLocalizedMessage, Snackbar.LENGTH_LONG).show()
false
}
def onPreferenceDataStoreChanged(store: PreferenceDataStore, key: String): Unit =
if (key != Key.proxyApps && findPreference(key) != null) app.dataStore.dirty = true
override def onDestroy() {
app.dataStore.unregisterChangeListener(this)
super.onDestroy()
}
override def onResume() {
super.onResume()
isProxyApps.setChecked(app.dataStore.proxyApps) // fetch proxyApps updated by AppManager
}
private def showPluginEditor() {
val bundle = new Bundle()
bundle.putString("key", Key.pluginConfigure)
bundle.putString(PluginConfigurationDialogFragment.PLUGIN_ID_FRAGMENT_TAG, pluginConfiguration.selected)
val fragment = new PluginConfigurationDialogFragment
fragment.setArguments(bundle)
fragment.setTargetFragment(this, 0)
fragment.show(getFragmentManager, FRAGMENT_DIALOG_TAG)
}
override def onDisplayPreferenceDialog(preference: Preference): Unit = if (preference.getKey == Key.pluginConfigure) {
val intent = PluginManager.buildIntent(pluginConfiguration.selected, PluginContract.ACTION_CONFIGURE)
if (intent.resolveActivity(getActivity.getPackageManager) != null)
startActivityForResult(intent.putExtra(PluginContract.EXTRA_OPTIONS,
pluginConfiguration.selectedOptions.toString), REQUEST_CODE_PLUGIN_CONFIGURE) else {
showPluginEditor()
}
} else super.onDisplayPreferenceDialog(preference)
override def onActivityResult(requestCode: Int, resultCode: Int, data: Intent): Unit = requestCode match {
case REQUEST_CODE_PLUGIN_CONFIGURE => resultCode match {
case Activity.RESULT_OK =>
val options = data.getStringExtra(PluginContract.EXTRA_OPTIONS)
pluginConfigure.setText(options)
onPluginConfigureChanged(null, options)
case PluginContract.RESULT_FALLBACK => showPluginEditor()
case _ =>
}
case _ => super.onActivityResult(requestCode, resultCode, data)
}
override def onMenuItemClick(item: MenuItem): Boolean = item.getItemId match {
case R.id.action_delete =>
new AlertDialog.Builder(getActivity)
.setTitle(R.string.delete_confirm_prompt)
.setPositiveButton(R.string.yes, ((_, _) => {
app.profileManager.delProfile(profile.id)
getActivity.finish()
}): DialogInterface.OnClickListener)
.setNegativeButton(R.string.no, null)
.create()
.show()
true
case R.id.action_apply =>
saveAndExit()
true
case _ => false
}
def saveAndExit() {
profile.deserialize(app.dataStore)
app.profileManager.updateProfile(profile)
if (ProfilesFragment.instance != null) ProfilesFragment.instance.profilesAdapter.deepRefreshId(profile.id)
getActivity.finish()
}
}
/*******************************************************************************/
/* */
/* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* */
/* This program is free software: you can redistribute it and/or modify */
/* it under the terms of the GNU General Public License as published by */
/* the Free Software Foundation, either version 3 of the License, or */
/* (at your option) any later version. */
/* */
/* This program is distributed in the hope that it will be useful, */
/* but WITHOUT ANY WARRANTY; without even the implied warranty of */
/* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the */
/* GNU General Public License for more details. */
/* */
/* You should have received a copy of the GNU General Public License */
/* along with this program. If not, see <http://www.gnu.org/licenses/>. */
/* */
/*******************************************************************************/
package com.github.shadowsocks
import android.content._
import android.os.Bundle
import android.support.design.widget.Snackbar
import android.support.v7.widget.RecyclerView.ViewHolder
import android.support.v7.widget._
import android.support.v7.widget.helper.ItemTouchHelper
import android.support.v7.widget.helper.ItemTouchHelper.SimpleCallback
import android.view.View.OnLongClickListener
import android.view._
import android.widget.{LinearLayout, PopupMenu, TextView, Toast}
import com.github.shadowsocks.ShadowsocksApplication.app
import com.github.shadowsocks.bg.{ServiceState, TrafficMonitor}
import com.github.shadowsocks.database.Profile
import com.github.shadowsocks.plugin.PluginConfiguration
import com.github.shadowsocks.utils._
import com.github.shadowsocks.widget.UndoSnackbarManager
import com.google.android.gms.ads.{AdRequest, AdSize, AdView}
import scala.collection.mutable.{ArrayBuffer, ListBuffer}
object ProfilesFragment {
var instance: ProfilesFragment = _ // used for callback from ProfileManager and stateChanged from MainActivity
}
final class ProfilesFragment extends ToolbarFragment with Toolbar.OnMenuItemClickListener {
import ProfilesFragment._
private val cardButtonLongClickListener: OnLongClickListener = view => {
Utils.positionToast(Toast.makeText(getActivity, view.getContentDescription, Toast.LENGTH_SHORT), view,
getActivity.getWindow, 0, getResources.getDimensionPixelOffset(R.dimen.margin_small)).show()
true
}
/**
* Is ProfilesFragment editable at all.
*/
private def isEnabled = getActivity.asInstanceOf[MainActivity].state match {
case ServiceState.CONNECTED | ServiceState.STOPPED => true
case _ => false
}
private def isProfileEditable(id: => Int) = getActivity.asInstanceOf[MainActivity].state match {
case ServiceState.CONNECTED => id != app.dataStore.profileId
case ServiceState.STOPPED => true
case _ => false
}
final class ProfileViewHolder(view: View) extends RecyclerView.ViewHolder(view)
with View.OnClickListener with PopupMenu.OnMenuItemClickListener {
var item: Profile = _
private val text1 = itemView.findViewById[TextView](android.R.id.text1)
private val text2 = itemView.findViewById[TextView](android.R.id.text2)
private val traffic = itemView.findViewById[TextView](R.id.traffic)
private val edit = itemView.findViewById[View](R.id.edit)
edit.setOnClickListener(_ => startConfig(item.id))
edit.setOnLongClickListener(cardButtonLongClickListener)
itemView.setOnClickListener(this)
private var adView: AdView = _
{
val share = itemView.findViewById[View](R.id.share)
share.setOnClickListener(_ => {
val popup = new PopupMenu(getActivity, share)
popup.getMenuInflater.inflate(R.menu.profile_share_popup, popup.getMenu)
popup.setOnMenuItemClickListener(this)
popup.show()
})
share.setOnLongClickListener(cardButtonLongClickListener)
}
def bind(item: Profile) {
this.item = item
val editable = isProfileEditable(item.id)
edit.setEnabled(editable)
edit.setAlpha(if (editable) 1 else 0.5F)
var tx = item.tx
var rx = item.rx
if (item.id == bandwidthProfile) {
tx += txTotal
rx += rxTotal
}
text1.setText(item.getName)
val t2 = new ListBuffer[String]
if (!item.nameIsEmpty) t2 += item.formattedAddress
new PluginConfiguration(item.plugin).selected match {
case "" =>
case id => t2 += app.getString(R.string.profile_plugin, id)
}
if (t2.isEmpty) text2.setVisibility(View.GONE) else {
text2.setVisibility(View.VISIBLE)
text2.setText(t2.mkString("\n"))
}
if (tx <= 0 && rx <= 0) traffic.setVisibility(View.GONE) else {
traffic.setVisibility(View.VISIBLE)
traffic.setText(getString(R.string.stat_profiles,
TrafficMonitor.formatTraffic(tx), TrafficMonitor.formatTraffic(rx)))
}
if (item.id == app.dataStore.profileId) {
itemView.setSelected(true)
selectedItem = this
} else {
itemView.setSelected(false)
if (selectedItem eq this) selectedItem = null
}
if (item.host == "198.199.101.152") {
if (adView == null) {
val params =
new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)
params.gravity = Gravity.CENTER_HORIZONTAL
adView = new AdView(getActivity)
adView.setLayoutParams(params)
adView.setAdUnitId("ca-app-pub-9097031975646651/7760346322")
adView.setAdSize(AdSize.FLUID)
itemView.findViewById[LinearLayout](R.id.content).addView(adView)
// Load Ad
val adBuilder = new AdRequest.Builder()
adBuilder.addTestDevice("B08FC1764A7B250E91EA9D0D5EBEB208")
adView.loadAd(adBuilder.build())
} else adView.setVisibility(View.VISIBLE)
} else if (adView != null) adView.setVisibility(View.GONE)
}
def onClick(v: View): Unit = if (isEnabled) {
val activity = getActivity.asInstanceOf[MainActivity]
val old = app.dataStore.profileId
app.switchProfile(item.id)
profilesAdapter.refreshId(old)
itemView.setSelected(true)
if (activity.state == ServiceState.CONNECTED) Utils.reloadSsService(activity)
}
override def onMenuItemClick(menu: MenuItem): Boolean = menu.getItemId match {
case R.id.action_qr_code_nfc =>
getFragmentManager.beginTransaction().add(new QRCodeDialog(item.toString), "").commitAllowingStateLoss()
true
case R.id.action_export =>
clipboard.setPrimaryClip(ClipData.newPlainText(null, item.toString))
true
case _ => false
}
}
final class ProfilesAdapter extends RecyclerView.Adapter[ProfileViewHolder] {
var profiles = new ArrayBuffer[Profile]
profiles ++= app.profileManager.getAllProfiles.getOrElse(List.empty[Profile])
def getItemCount: Int = profiles.length
def onBindViewHolder(vh: ProfileViewHolder, i: Int): Unit = vh.bind(profiles(i))
def onCreateViewHolder(vg: ViewGroup, i: Int) =
new ProfileViewHolder(LayoutInflater.from(vg.getContext).inflate(R.layout.layout_profile, vg, false))
setHasStableIds(true) // Reason: http://stackoverflow.com/a/32488059/2245107
override def getItemId(position: Int): Long = profiles(position).id
def add(item: Profile) {
undoManager.flush()
val pos = getItemCount
profiles += item
notifyItemInserted(pos)
}
def move(from: Int, to: Int) {
undoManager.flush()
val step = if (from < to) 1 else -1
val first = profiles(from)
var previousOrder = profiles(from).userOrder
for (i <- from until to by step) {
val next = profiles(i + step)
val order = next.userOrder
next.userOrder = previousOrder
previousOrder = order
profiles(i) = next
app.profileManager.updateProfile(next)
}
first.userOrder = previousOrder
profiles(to) = first
app.profileManager.updateProfile(first)
notifyItemMoved(from, to)
}
def remove(pos: Int) {
profiles.remove(pos)
notifyItemRemoved(pos)
}
def undo(actions: Iterator[(Int, Profile)]): Unit = for ((index, item) <- actions) {
profiles.insert(index, item)
notifyItemInserted(index)
}
def commit(actions: Iterator[(Int, Profile)]): Unit =
for ((_, item) <- actions) app.profileManager.delProfile(item.id)
def refreshId(id: Int) {
val index = profiles.indexWhere(_.id == id)
if (index >= 0) notifyItemChanged(index)
}
def deepRefreshId(id: Int) {
val index = profiles.indexWhere(_.id == id)
if (index >= 0) {
profiles(index) = app.profileManager.getProfile(id).get
notifyItemChanged(index)
}
}
def removeId(id: Int) {
val index = profiles.indexWhere(_.id == id)
if (index >= 0) {
profiles.remove(index)
notifyItemRemoved(index)
if (id == app.dataStore.profileId) app.dataStore.profileId = 0 // switch to null profile
}
}
}
private var selectedItem: ProfileViewHolder = _
lazy val profilesAdapter = new ProfilesAdapter
private var undoManager: UndoSnackbarManager[Profile] = _
private var bandwidthProfile: Int = _
private var txTotal: Long = _
private var rxTotal: Long = _
private lazy val clipboard = getActivity.getSystemService(Context.CLIPBOARD_SERVICE).asInstanceOf[ClipboardManager]
private def startConfig(id: Int) = startActivity(new Intent(getActivity, classOf[ProfileConfigActivity])
.putExtra(Action.EXTRA_PROFILE_ID, id))
override def onCreateView(inflater: LayoutInflater, container: ViewGroup, savedInstanceState: Bundle): View =
inflater.inflate(R.layout.layout_list, container, false)
override def onViewCreated(view: View, savedInstanceState: Bundle) {
super.onViewCreated(view, savedInstanceState)
toolbar.setTitle(R.string.profiles)
toolbar.inflateMenu(R.menu.profile_manager_menu)
toolbar.setOnMenuItemClickListener(this)
if (app.profileManager.getFirstProfile.isEmpty) app.dataStore.profileId = app.profileManager.createProfile().id
val profilesList = view.findViewById[RecyclerView](R.id.list)
val layoutManager = new LinearLayoutManager(getActivity, LinearLayoutManager.VERTICAL, false)
profilesList.setLayoutManager(layoutManager)
profilesList.addItemDecoration(new DividerItemDecoration(getActivity, layoutManager.getOrientation))
layoutManager.scrollToPosition(profilesAdapter.profiles.zipWithIndex.collectFirst {
case (profile, i) if profile.id == app.dataStore.profileId => i
}.getOrElse(-1))
val animator = new DefaultItemAnimator()
animator.setSupportsChangeAnimations(false) // prevent fading-in/out when rebinding
profilesList.setItemAnimator(animator)
profilesList.setAdapter(profilesAdapter)
instance = this
undoManager = new UndoSnackbarManager[Profile](getActivity.findViewById(R.id.snackbar),
profilesAdapter.undo, profilesAdapter.commit)
new ItemTouchHelper(new SimpleCallback(ItemTouchHelper.UP | ItemTouchHelper.DOWN,
ItemTouchHelper.START | ItemTouchHelper.END) {
override def getSwipeDirs(recyclerView: RecyclerView, viewHolder: ViewHolder): Int =
if (isProfileEditable(viewHolder.asInstanceOf[ProfileViewHolder].item.id))
super.getSwipeDirs(recyclerView, viewHolder) else 0
override def getDragDirs(recyclerView: RecyclerView, viewHolder: ViewHolder): Int =
if (isEnabled) super.getDragDirs(recyclerView, viewHolder) else 0
def onSwiped(viewHolder: ViewHolder, direction: Int) {
val index = viewHolder.getAdapterPosition
profilesAdapter.remove(index)
undoManager.remove((index, viewHolder.asInstanceOf[ProfileViewHolder].item))
}
def onMove(recyclerView: RecyclerView, viewHolder: ViewHolder, target: ViewHolder): Boolean = {
profilesAdapter.move(viewHolder.getAdapterPosition, target.getAdapterPosition)
true
}
}).attachToRecyclerView(profilesList)
}
override def onTrafficUpdated(profileId: Int, txRate: Long, rxRate: Long, txTotal: Long, rxTotal: Long): Unit =
if (profileId != -1) { // ignore resets from MainActivity
if (bandwidthProfile != profileId) {
onTrafficPersisted(bandwidthProfile)
bandwidthProfile = profileId
}
this.txTotal = txTotal
this.rxTotal = rxTotal
profilesAdapter.refreshId(profileId)
}
def onTrafficPersisted(profileId: Int) {
txTotal = 0
rxTotal = 0
if (bandwidthProfile != profileId) {
onTrafficPersisted(bandwidthProfile)
bandwidthProfile = profileId
}
profilesAdapter.deepRefreshId(profileId)
}
override def onDetach() {
undoManager.flush()
super.onDetach()
}
override def onDestroy() {
instance = null
super.onDestroy()
}
def onMenuItemClick(item: MenuItem): Boolean = item.getItemId match {
case R.id.action_scan_qr_code =>
startActivity(new Intent(getActivity, classOf[ScannerActivity]))
true
case R.id.action_import =>
try {
val profiles = Parser.findAll(clipboard.getPrimaryClip.getItemAt(0).getText)
if (profiles.nonEmpty) {
profiles.foreach(app.profileManager.createProfile)
Toast.makeText(getActivity, R.string.action_import_msg, Toast.LENGTH_SHORT).show()
return true
}
} catch {
case _: Exception =>
}
Snackbar.make(getActivity.findViewById(R.id.snackbar), R.string.action_import_err, Snackbar.LENGTH_LONG).show()
true
case R.id.action_manual_settings =>
startConfig(app.profileManager.createProfile().id)
true
case R.id.action_export =>
app.profileManager.getAllProfiles match {
case Some(profiles) =>
clipboard.setPrimaryClip(ClipData.newPlainText(null, profiles.mkString("\n")))
Toast.makeText(getActivity, R.string.action_export_msg, Toast.LENGTH_SHORT).show()
case _ => Toast.makeText(getActivity, R.string.action_export_err, Toast.LENGTH_SHORT).show()
}
true
case _ => false
}
}
/*******************************************************************************/
/* */
/* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* */
/* This program is free software: you can redistribute it and/or modify */
/* it under the terms of the GNU General Public License as published by */
/* the Free Software Foundation, either version 3 of the License, or */
/* (at your option) any later version. */
/* */
/* This program is distributed in the hope that it will be useful, */
/* but WITHOUT ANY WARRANTY; without even the implied warranty of */
/* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the */
/* GNU General Public License for more details. */
/* */
/* You should have received a copy of the GNU General Public License */
/* along with this program. If not, see <http://www.gnu.org/licenses/>. */
/* */
/*******************************************************************************/
package com.github.shadowsocks
import java.nio.charset.Charset
import android.app.Activity
import android.content.Context
import android.nfc.{NdefMessage, NdefRecord, NfcAdapter}
import android.os.Bundle
import android.support.v4.app.DialogFragment
import android.view.{LayoutInflater, View, ViewGroup}
import android.widget.{ImageView, LinearLayout}
import net.glxn.qrgen.android.QRCode
object QRCodeDialog {
private final val KEY_URL = "com.github.shadowsocks.QRCodeDialog.KEY_URL"
}
final class QRCodeDialog extends DialogFragment {
import QRCodeDialog._
def this(url: String) {
this()
val bundle = new Bundle()
bundle.putString(KEY_URL, url)
setArguments(bundle)
}
private def url = getArguments.getString(KEY_URL)
private lazy val nfcShareItem = url.getBytes(Charset.forName("UTF-8"))
private var adapter: NfcAdapter = _
override def onCreateView(inflater: LayoutInflater, container: ViewGroup, savedInstanceState: Bundle): View = {
val image = new ImageView(getActivity)
image.setLayoutParams(new LinearLayout.LayoutParams(-1, -1))
val size = getResources.getDimensionPixelSize(R.dimen.qr_code_size)
val qrcode = QRCode.from(url)
.withSize(size, size)
.asInstanceOf[QRCode].bitmap()
image.setImageBitmap(qrcode)
image
}
override def onAttach(context: Context) {
super.onAttach(context)
adapter = NfcAdapter.getDefaultAdapter(getActivity)
if (adapter != null) adapter.setNdefPushMessage(new NdefMessage(Array(
new NdefRecord(NdefRecord.TNF_ABSOLUTE_URI, nfcShareItem, Array[Byte](), nfcShareItem))),
context.asInstanceOf[Activity])
}
override def onDetach() {
if (adapter != null) {
adapter.setNdefPushMessage(null, getActivity)
adapter = null
}
super.onDetach()
}
}
/*******************************************************************************/
/* */
/* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* */
/* This program is free software: you can redistribute it and/or modify */
/* it under the terms of the GNU General Public License as published by */
/* the Free Software Foundation, either version 3 of the License, or */
/* (at your option) any later version. */
/* */
/* This program is distributed in the hope that it will be useful, */
/* but WITHOUT ANY WARRANTY; without even the implied warranty of */
/* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the */
/* GNU General Public License for more details. */
/* */
/* You should have received a copy of the GNU General Public License */
/* along with this program. If not, see <http://www.gnu.org/licenses/>. */
/* */
/*******************************************************************************/
package com.github.shadowsocks
import android.app.Activity
import android.content.Intent
import android.content.pm.ShortcutManager
import android.os.{Build, Bundle}
import android.support.v4.content.pm.{ShortcutInfoCompat, ShortcutManagerCompat}
import android.support.v4.graphics.drawable.IconCompat
import com.github.shadowsocks.bg.ServiceState
import com.github.shadowsocks.utils.Utils
/**
* @author Mygod
*/
class QuickToggleShortcut extends Activity with ServiceBoundContext {
override def onCreate(savedInstanceState: Bundle) {
super.onCreate(savedInstanceState)
getIntent.getAction match {
case Intent.ACTION_CREATE_SHORTCUT =>
setResult(Activity.RESULT_OK, ShortcutManagerCompat.createShortcutResultIntent(this,
new ShortcutInfoCompat.Builder(this, "toggle")
.setIntent(new Intent(this, classOf[QuickToggleShortcut]).setAction(Intent.ACTION_MAIN))
.setIcon(IconCompat.createWithResource(this, R.drawable.ic_qu_shadowsocks_launcher))
.setShortLabel(getString(R.string.quick_toggle))
.build()))
finish()
case _ =>
attachService()
if (Build.VERSION.SDK_INT >= 25) getSystemService(classOf[ShortcutManager]).reportShortcutUsed("toggle")
}
}
override def onDestroy() {
detachService()
super.onDestroy()
}
override def onServiceConnected() {
bgService.getState match {
case ServiceState.STOPPED => Utils.startSsService(this)
case ServiceState.CONNECTED => Utils.stopSsService(this)
case _ => // ignore
}
finish()
}
}
/*******************************************************************************/
/* */
/* Copyright (C) 2016 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2016 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* */
/* This program is free software: you can redistribute it and/or modify */
/* it under the terms of the GNU General Public License as published by */
/* the Free Software Foundation, either version 3 of the License, or */
/* (at your option) any later version. */
/* */
/* This program is distributed in the hope that it will be useful, */
/* but WITHOUT ANY WARRANTY; without even the implied warranty of */
/* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the */
/* GNU General Public License for more details. */
/* */
/* You should have received a copy of the GNU General Public License */
/* along with this program. If not, see <http://www.gnu.org/licenses/>. */
/* */
/*******************************************************************************/
package com.github.shadowsocks
import android.app.TaskStackBuilder
import android.content.pm.{PackageManager, ShortcutManager}
import android.os.{Build, Bundle}
import android.support.v4.app.ActivityCompat
import android.support.v4.content.ContextCompat
import android.support.v7.app.AppCompatActivity
import android.support.v7.widget.Toolbar
import android.text.TextUtils
import android.widget.Toast
import com.github.shadowsocks.ShadowsocksApplication.app
import com.github.shadowsocks.utils.Parser
import com.google.zxing.Result
import me.dm7.barcodescanner.zxing.ZXingScannerView
object ScannerActivity {
private final val MY_PERMISSIONS_REQUEST_CAMERA = 1
}
class ScannerActivity extends AppCompatActivity with ZXingScannerView.ResultHandler {
import ScannerActivity._
private var scannerView: ZXingScannerView = _
override def onRequestPermissionsResult(requestCode: Int, permissions: Array[String],
grantResults: Array[Int]) {
if (requestCode == MY_PERMISSIONS_REQUEST_CAMERA) {
// If request is cancelled, the result arrays are empty.
if (grantResults.length > 0
&& grantResults(0) == PackageManager.PERMISSION_GRANTED) {
scannerView.setResultHandler(this)
scannerView.startCamera()
} else {
Toast.makeText(this, R.string.add_profile_scanner_permission_required, Toast.LENGTH_SHORT).show()
finish()
}
}
}
def navigateUp() {
val intent = getParentActivityIntent
if (shouldUpRecreateTask(intent) || isTaskRoot)
TaskStackBuilder.create(this).addNextIntentWithParentStack(intent).startActivities()
else finish()
}
override def onCreate(state: Bundle) {
super.onCreate(state)
setContentView(R.layout.layout_scanner)
val toolbar = findViewById(R.id.toolbar).asInstanceOf[Toolbar]
toolbar.setTitle(getTitle)
toolbar.setNavigationIcon(R.drawable.abc_ic_ab_back_material)
toolbar.setNavigationOnClickListener(_ => navigateUp())
scannerView = findViewById(R.id.scanner).asInstanceOf[ZXingScannerView]
if (Build.VERSION.SDK_INT >= 25) getSystemService(classOf[ShortcutManager]).reportShortcutUsed("scan")
}
override def onResume() {
super.onResume()
val permissionCheck = ContextCompat.checkSelfPermission(this,
android.Manifest.permission.CAMERA)
if (permissionCheck == PackageManager.PERMISSION_GRANTED) {
scannerView.setResultHandler(this) // Register ourselves as a handler for scan results.
scannerView.startCamera() // Start camera on resume
} else {
ActivityCompat.requestPermissions(this,
Array(android.Manifest.permission.CAMERA), MY_PERMISSIONS_REQUEST_CAMERA)
}
}
override def onPause() {
super.onPause()
scannerView.stopCamera() // Stop camera on pause
}
override def handleResult(rawResult: Result) = {
val uri = rawResult.getText
if (!TextUtils.isEmpty(uri))
Parser.findAll(uri).foreach(app.profileManager.createProfile)
navigateUp()
}
}
/*******************************************************************************/
/* */
/* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* */
/* This program is free software: you can redistribute it and/or modify */
/* it under the terms of the GNU General Public License as published by */
/* the Free Software Foundation, either version 3 of the License, or */
/* (at your option) any later version. */
/* */
/* This program is distributed in the hope that it will be useful, */
/* but WITHOUT ANY WARRANTY; without even the implied warranty of */
/* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the */
/* GNU General Public License for more details. */
/* */
/* You should have received a copy of the GNU General Public License */
/* along with this program. If not, see <http://www.gnu.org/licenses/>. */
/* */
/*******************************************************************************/
package com.github.shadowsocks
import android.content.{ComponentName, Context, Intent, ServiceConnection}
import android.os.{IBinder, RemoteException}
import com.github.shadowsocks.aidl.{IShadowsocksService, IShadowsocksServiceCallback}
import com.github.shadowsocks.utils.Action
import com.github.shadowsocks.ShadowsocksApplication.app
import com.github.shadowsocks.bg.{TransproxyService, VpnService}
/**
* @author Mygod
*/
trait ServiceBoundContext extends Context with IBinder.DeathRecipient {
private class ShadowsocksServiceConnection extends ServiceConnection {
override def onServiceConnected(name: ComponentName, service: IBinder) {
binder = service
service.linkToDeath(ServiceBoundContext.this, 0)
bgService = IShadowsocksService.Stub.asInterface(service)
if (callback != null && !callbackRegistered) try {
bgService.registerCallback(callback)
callbackRegistered = true
if (listeningForBandwidth) bgService.startListeningForBandwidth(callback)
} catch {
case _: RemoteException => // Nothing
}
ServiceBoundContext.this.onServiceConnected()
}
override def onServiceDisconnected(name: ComponentName) {
unregisterCallback()
ServiceBoundContext.this.onServiceDisconnected()
bgService = null
binder = null
}
}
protected def setListeningForBandwidth(value: Boolean) {
if (listeningForBandwidth != value && bgService != null && callback != null)
if (value) bgService.startListeningForBandwidth(callback) else bgService.stopListeningForBandwidth(callback)
listeningForBandwidth = value
}
private def unregisterCallback() {
if (bgService != null && callback != null && callbackRegistered) try bgService.unregisterCallback(callback) catch {
case _: RemoteException =>
}
callbackRegistered = false
}
protected def onServiceConnected(): Unit = ()
/**
* Different from Android framework, this method will be called even when you call `detachService`.
*/
protected def onServiceDisconnected(): Unit = ()
override def binderDied(): Unit = ()
private var callback: IShadowsocksServiceCallback.Stub = _
private var connection: ShadowsocksServiceConnection = _
private var callbackRegistered: Boolean = _
private var listeningForBandwidth: Boolean = _
// Variables
private var binder: IBinder = _
var bgService: IShadowsocksService = _
protected def attachService(callback: IShadowsocksServiceCallback.Stub = null) {
this.callback = callback
if (bgService == null) {
val intent = new Intent(this, app.serviceClass)
intent.setAction(Action.SERVICE)
connection = new ShadowsocksServiceConnection()
bindService(intent, connection, Context.BIND_AUTO_CREATE)
}
}
protected def detachService() {
unregisterCallback()
onServiceDisconnected()
callback = null
if (connection != null) {
try unbindService(connection) catch {
case _: IllegalArgumentException => // ignore
}
connection = null
}
if (binder != null) {
binder.unlinkToDeath(this, 0)
binder = null
}
bgService = null
}
}
/*******************************************************************************/
/* */
/* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* */
/* This program is free software: you can redistribute it and/or modify */
/* it under the terms of the GNU General Public License as published by */
/* the Free Software Foundation, either version 3 of the License, or */
/* (at your option) any later version. */
/* */
/* This program is distributed in the hope that it will be useful, */
/* but WITHOUT ANY WARRANTY; without even the implied warranty of */
/* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the */
/* GNU General Public License for more details. */
/* */
/* You should have received a copy of the GNU General Public License */
/* along with this program. If not, see <http://www.gnu.org/licenses/>. */
/* */
/*******************************************************************************/
package com.github.shadowsocks
import java.io.{File, FileOutputStream, IOException}
import java.util.Locale
import android.annotation.SuppressLint
import android.app.{Application, NotificationChannel, NotificationManager}
import android.content._
import android.content.pm.{PackageInfo, PackageManager}
import android.content.res.Configuration
import android.os.{Build, LocaleList}
import android.support.v7.app.AppCompatDelegate
import android.util.Log
import com.evernote.android.job.JobManager
import com.github.shadowsocks.acl.DonaldTrump
import com.github.shadowsocks.bg.{ProxyService, TransproxyService, VpnService}
import com.github.shadowsocks.database.{DBHelper, Profile, ProfileManager}
import com.github.shadowsocks.preference.{BottomSheetPreferenceDialogFragment, IconListPreference, OrmLitePreferenceDataStore}
import com.github.shadowsocks.utils.CloseUtils._
import com.github.shadowsocks.utils._
import com.google.android.gms.analytics.{GoogleAnalytics, HitBuilders, StandardExceptionParser, Tracker}
import com.google.firebase.FirebaseApp
import com.google.firebase.remoteconfig.FirebaseRemoteConfig
import com.j256.ormlite.logger.LocalLog
import com.takisoft.fix.support.v7.preference.PreferenceFragmentCompat
import scala.collection.JavaConversions._
object ShadowsocksApplication {
var app: ShadowsocksApplication = _
private final val TAG = "ShadowsocksApplication"
// The ones in Locale doesn't have script included
private final lazy val SIMPLIFIED_CHINESE =
if (Build.VERSION.SDK_INT >= 21) Locale.forLanguageTag("zh-Hans-CN") else Locale.SIMPLIFIED_CHINESE
private final lazy val TRADITIONAL_CHINESE =
if (Build.VERSION.SDK_INT >= 21) Locale.forLanguageTag("zh-Hant-TW") else Locale.TRADITIONAL_CHINESE
}
class ShadowsocksApplication extends Application {
import ShadowsocksApplication._
lazy val remoteConfig: FirebaseRemoteConfig = FirebaseRemoteConfig.getInstance()
lazy val tracker: Tracker = GoogleAnalytics.getInstance(this).newTracker(R.xml.tracker)
private lazy val dbHelper = new DBHelper(this)
lazy val profileManager = new ProfileManager(dbHelper)
lazy val dataStore = new OrmLitePreferenceDataStore(dbHelper)
def usingVpnMode: Boolean = dataStore.serviceMode == Key.modeVpn
def serviceClass: Class[_] = app.dataStore.serviceMode match {
case Key.modeProxy => classOf[ProxyService]
case Key.modeVpn => classOf[VpnService]
case Key.modeTransproxy => classOf[TransproxyService]
}
// send event
def track(category: String, action: String): Unit = tracker.send(new HitBuilders.EventBuilder()
.setAction(action)
.setLabel(BuildConfig.VERSION_NAME)
.build())
def track(t: Throwable): Unit = tracker.send(new HitBuilders.ExceptionBuilder()
.setDescription(new StandardExceptionParser(this, null).getDescription(Thread.currentThread.getName, t))
.setFatal(false)
.build())
def currentProfile: Option[Profile] = profileManager.getProfile(dataStore.profileId)
def switchProfile(id: Int): Profile = {
val result = profileManager.getProfile(id) getOrElse profileManager.createProfile()
dataStore.profileId = result.id
result
}
private def checkChineseLocale(locale: Locale): Locale = if (locale.getLanguage == "zh") locale.getCountry match {
case "CN" | "TW" => null
case _ => (if (Build.VERSION.SDK_INT >= 21) locale.getScript else null) match {
case "Hans" => SIMPLIFIED_CHINESE
case "Hant" => TRADITIONAL_CHINESE
case script =>
Log.w(TAG, "Unknown zh locale script: %s. Falling back to trying countries...".format(script))
locale.getCountry match {
case "SG" => SIMPLIFIED_CHINESE
case "HK" | "MO" => TRADITIONAL_CHINESE
case _ =>
Log.w(TAG, "Unknown zh locale: %s. Falling back to zh-Hans-CN..."
.formatLocal(Locale.ENGLISH, if (Build.VERSION.SDK_INT >= 21) locale.toLanguageTag else locale))
SIMPLIFIED_CHINESE
}
}
} else null
@SuppressLint(Array("NewApi"))
private def checkChineseLocale(config: Configuration): Unit = if (Build.VERSION.SDK_INT >= 24) {
val localeList = config.getLocales
val newList = new Array[Locale](localeList.size())
var changed = false
for (i <- 0 until localeList.size()) {
val locale = localeList.get(i)
val newLocale = checkChineseLocale(locale)
if (newLocale == null) newList(i) = locale else {
newList(i) = newLocale
changed = true
}
}
if (changed) {
val newConfig = new Configuration(config)
newConfig.setLocales(new LocaleList(newList.distinct: _*))
val res = getResources
res.updateConfiguration(newConfig, res.getDisplayMetrics)
}
} else {
//noinspection ScalaDeprecation
val newLocale = checkChineseLocale(config.locale)
if (newLocale != null) {
val newConfig = new Configuration(config)
//noinspection ScalaDeprecation
newConfig.locale = newLocale
val res = getResources
res.updateConfiguration(newConfig, res.getDisplayMetrics)
}
}
override def onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
checkChineseLocale(newConfig)
}
override def onCreate() {
app = this
if (!BuildConfig.DEBUG) java.lang.System.setProperty(LocalLog.LOCAL_LOG_LEVEL_PROPERTY, "ERROR")
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true)
checkChineseLocale(getResources.getConfiguration)
FirebaseApp.initializeApp(this)
remoteConfig.setDefaults(R.xml.default_configs)
remoteConfig.fetch().addOnCompleteListener(task =>
if (task.isSuccessful) remoteConfig.activateFetched() else Log.e(TAG, "Failed to fetch config"))
JobManager.create(this).addJobCreator(DonaldTrump)
PreferenceFragmentCompat.registerPreferenceFragment(classOf[IconListPreference],
classOf[BottomSheetPreferenceDialogFragment])
TcpFastOpen.enabled(dataStore.getBoolean(Key.tfo, TcpFastOpen.sendEnabled))
if (dataStore.getLong(Key.assetUpdateTime, -1) != info.lastUpdateTime) copyAssets()
if (Build.VERSION.SDK_INT >= 26) {
val nm = getSystemService(classOf[NotificationManager])
nm.createNotificationChannels(List(
new NotificationChannel("service-vpn", getText(R.string.service_vpn), NotificationManager.IMPORTANCE_LOW),
new NotificationChannel("service-proxy", getText(R.string.service_proxy), NotificationManager.IMPORTANCE_LOW),
new NotificationChannel("service-transproxy", getText(R.string.service_transproxy),
NotificationManager.IMPORTANCE_LOW)
))
nm.deleteNotificationChannel("service-nat") // NAT mode is gone for good
}
}
lazy val info: PackageInfo = getPackageManager.getPackageInfo(getPackageName, PackageManager.GET_SIGNATURES)
def copyAssets() {
val assetManager = getAssets
for (dir <- Array("acl", "overture")) {
var files: Array[String] = null
try files = assetManager.list(dir) catch {
case e: IOException =>
Log.e(TAG, e.getMessage)
app.track(e)
}
if (files != null) for (file <- files) autoClose(assetManager.open(dir + "/" + file))(in =>
autoClose(new FileOutputStream(new File(getFilesDir, file)))(out =>
IOUtils.copy(in, out)))
}
dataStore.putLong(Key.assetUpdateTime, info.lastUpdateTime)
}
def listenForPackageChanges(callback: => Unit): BroadcastReceiver = {
val filter = new IntentFilter(Intent.ACTION_PACKAGE_ADDED)
filter.addAction(Intent.ACTION_PACKAGE_REMOVED)
filter.addDataScheme("package")
val result: BroadcastReceiver = (_: Context, intent: Intent) =>
if (intent.getAction != Intent.ACTION_PACKAGE_REMOVED || !intent.getBooleanExtra(Intent.EXTRA_REPLACING, false))
callback
app.registerReceiver(result, filter)
result
}
}
/*******************************************************************************/
/* */
/* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* */
/* This program is free software: you can redistribute it and/or modify */
/* it under the terms of the GNU General Public License as published by */
/* the Free Software Foundation, either version 3 of the License, or */
/* (at your option) any later version. */
/* */
/* This program is distributed in the hope that it will be useful, */
/* but WITHOUT ANY WARRANTY; without even the implied warranty of */
/* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the */
/* GNU General Public License for more details. */
/* */
/* You should have received a copy of the GNU General Public License */
/* along with this program. If not, see <http://www.gnu.org/licenses/>. */
/* */
/*******************************************************************************/
package com.github.shadowsocks
import android.app.backup.{BackupAgentHelper, FileBackupHelper}
import com.github.shadowsocks.acl.Acl
import com.github.shadowsocks.database.DBHelper
class ShadowsocksBackupAgent extends BackupAgentHelper {
override def onCreate(): Unit = addHelper("com.github.shadowsocks.database.profile",
new FileBackupHelper(this, "../databases/" + DBHelper.PROFILE, Acl.CUSTOM_RULES + ".acl"))
}
/*******************************************************************************/
/* */
/* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* */
/* This program is free software: you can redistribute it and/or modify */
/* it under the terms of the GNU General Public License as published by */
/* the Free Software Foundation, either version 3 of the License, or */
/* (at your option) any later version. */
/* */
/* This program is distributed in the hope that it will be useful, */
/* but WITHOUT ANY WARRANTY; without even the implied warranty of */
/* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the */
/* GNU General Public License for more details. */
/* */
/* You should have received a copy of the GNU General Public License */
/* along with this program. If not, see <http://www.gnu.org/licenses/>. */
/* */
/*******************************************************************************/
package com.github.shadowsocks
import android.app.{Activity, KeyguardManager}
import android.content.{BroadcastReceiver, Context, Intent, IntentFilter}
import android.net.VpnService
import android.os.{Bundle, Handler}
import android.util.Log
import com.github.shadowsocks.ShadowsocksApplication.app
import com.github.shadowsocks.utils.Utils
object ShadowsocksRunnerActivity {
private final val TAG = "ShadowsocksRunnerActivity"
private final val REQUEST_CONNECT = 1
}
class ShadowsocksRunnerActivity extends Activity with ServiceBoundContext {
import ShadowsocksRunnerActivity._
val handler = new Handler()
// Variables
var receiver: BroadcastReceiver = _
override def onServiceConnected() {
handler.postDelayed(() => if (bgService != null) startBackgroundService(), 1000)
}
def startBackgroundService() {
if (app.usingVpnMode) VpnService.prepare(ShadowsocksRunnerActivity.this) match {
case null => onActivityResult(REQUEST_CONNECT, Activity.RESULT_OK, null)
case intent => startActivityForResult(intent, REQUEST_CONNECT)
} else {
Utils.startSsService(this)
finish()
}
}
override def onCreate(savedInstanceState: Bundle) {
super.onCreate(savedInstanceState)
val km = getSystemService(Context.KEYGUARD_SERVICE).asInstanceOf[KeyguardManager]
val locked = km.inKeyguardRestrictedInputMode
if (locked) {
val filter = new IntentFilter(Intent.ACTION_USER_PRESENT)
receiver = (_: Context, intent: Intent) => {
if (intent.getAction == Intent.ACTION_USER_PRESENT) {
attachService()
}
}
registerReceiver(receiver, filter)
} else {
attachService()
}
}
override def onDestroy() {
super.onDestroy()
detachService()
if (receiver != null) {
unregisterReceiver(receiver)
receiver = null
}
}
override def onActivityResult(requestCode: Int, resultCode: Int, data: Intent) {
resultCode match {
case Activity.RESULT_OK => Utils.startSsService(this)
case _ =>
Log.e(TAG, "Failed to start VpnService")
}
finish()
}
}
/*******************************************************************************/
/* */
/* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* */
/* This program is free software: you can redistribute it and/or modify */
/* it under the terms of the GNU General Public License as published by */
/* the Free Software Foundation, either version 3 of the License, or */
/* (at your option) any later version. */
/* */
/* This program is distributed in the hope that it will be useful, */
/* but WITHOUT ANY WARRANTY; without even the implied warranty of */
/* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the */
/* GNU General Public License for more details. */
/* */
/* You should have received a copy of the GNU General Public License */
/* along with this program. If not, see <http://www.gnu.org/licenses/>. */
/* */
/*******************************************************************************/
package com.github.shadowsocks
import android.app.Activity
import android.content.res.Resources
import android.os.{Build, Bundle}
import android.support.v7.app.AppCompatActivity
import android.support.v7.widget.{DefaultItemAnimator, LinearLayoutManager, RecyclerView, Toolbar}
import android.view.{LayoutInflater, View, ViewGroup}
import android.widget.{CheckedTextView, Switch}
import com.github.shadowsocks.database.Profile
import com.github.shadowsocks.utils.TaskerSettings
import com.github.shadowsocks.ShadowsocksApplication.app
/**
* @author CzBiX
*/
class TaskerActivity extends AppCompatActivity {
private class ProfileViewHolder(view: View) extends RecyclerView.ViewHolder(view) with View.OnClickListener {
{
val typedArray = obtainStyledAttributes(Array(android.R.attr.selectableItemBackground))
view.setBackgroundResource(typedArray.getResourceId(0, 0))
typedArray.recycle()
}
private var item: Profile = _
private val text = itemView.findViewById[CheckedTextView](android.R.id.text1)
itemView.setOnClickListener(this)
def bindDefault() {
item = null
text.setText(R.string.profile_default)
text.setChecked(taskerOption.profileId < 0)
}
def bind(item: Profile) {
this.item = item
text.setText(item.getName)
text.setChecked(taskerOption.profileId == item.id)
}
def onClick(v: View) {
taskerOption.switchOn = switch.isChecked
taskerOption.profileId = if (item == null) -1 else item.id
setResult(Activity.RESULT_OK, taskerOption.toIntent(TaskerActivity.this))
finish()
}
}
private class ProfilesAdapter extends RecyclerView.Adapter[ProfileViewHolder] {
val profiles: List[Profile] = app.profileManager.getAllProfiles.getOrElse(List.empty[Profile])
def getItemCount: Int = 1 + profiles.length
def onBindViewHolder(vh: ProfileViewHolder, i: Int): Unit = i match {
case 0 => vh.bindDefault()
case _ => vh.bind(profiles(i - 1))
}
private val name = "select_dialog_singlechoice_" + (if (Build.VERSION.SDK_INT >= 21) "material" else "holo")
def onCreateViewHolder(vg: ViewGroup, i: Int) = new ProfileViewHolder(LayoutInflater.from(vg.getContext)
.inflate(Resources.getSystem.getIdentifier(name, "layout", "android"), vg, false))
}
private var taskerOption: TaskerSettings = _
private var switch: Switch = _
private val profilesAdapter = new ProfilesAdapter
override def onCreate(savedInstanceState: Bundle) {
super.onCreate(savedInstanceState)
try taskerOption = TaskerSettings.fromIntent(getIntent) catch {
case _: Exception =>
finish()
return
}
setContentView(R.layout.layout_tasker)
val toolbar = findViewById(R.id.toolbar).asInstanceOf[Toolbar]
toolbar.setTitle(R.string.app_name)
toolbar.setNavigationIcon(R.drawable.ic_navigation_close)
toolbar.setNavigationOnClickListener(_ => finish())
switch = findViewById(R.id.serviceSwitch).asInstanceOf[Switch]
switch.setChecked(taskerOption.switchOn)
val profilesList = findViewById(R.id.list).asInstanceOf[RecyclerView]
val lm = new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false)
profilesList.setLayoutManager(lm)
profilesList.setItemAnimator(new DefaultItemAnimator)
profilesList.setAdapter(profilesAdapter)
if (taskerOption.profileId >= 0) lm.scrollToPosition(profilesAdapter.profiles.zipWithIndex.collectFirst {
case (profile, i) if profile.id == taskerOption.profileId => i + 1
}.getOrElse(0))
}
}
/*******************************************************************************/
/* */
/* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* */
/* This program is free software: you can redistribute it and/or modify */
/* it under the terms of the GNU General Public License as published by */
/* the Free Software Foundation, either version 3 of the License, or */
/* (at your option) any later version. */
/* */
/* This program is distributed in the hope that it will be useful, */
/* but WITHOUT ANY WARRANTY; without even the implied warranty of */
/* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the */
/* GNU General Public License for more details. */
/* */
/* You should have received a copy of the GNU General Public License */
/* along with this program. If not, see <http://www.gnu.org/licenses/>. */
/* */
/*******************************************************************************/
package com.github.shadowsocks
import android.content.{BroadcastReceiver, Context, Intent}
import com.github.shadowsocks.utils.{TaskerSettings, Utils}
import com.github.shadowsocks.ShadowsocksApplication.app
/**
* @author CzBiX
*/
class TaskerReceiver extends BroadcastReceiver {
override def onReceive(context: Context, intent: Intent) {
val settings = TaskerSettings.fromIntent(intent)
var changed = false
app.profileManager.getProfile(settings.profileId) match {
case Some(_) => if (app.dataStore.profileId != settings.profileId) {
app.switchProfile(settings.profileId)
changed = true
}
case _ =>
}
if (settings.switchOn) {
Utils.startSsService(context)
if (changed) Utils.reloadSsService(context)
} else Utils.stopSsService(context)
}
}
/*******************************************************************************/
/* */
/* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* */
/* This program is free software: you can redistribute it and/or modify */
/* it under the terms of the GNU General Public License as published by */
/* the Free Software Foundation, either version 3 of the License, or */
/* (at your option) any later version. */
/* */
/* This program is distributed in the hope that it will be useful, */
/* but WITHOUT ANY WARRANTY; without even the implied warranty of */
/* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the */
/* GNU General Public License for more details. */
/* */
/* You should have received a copy of the GNU General Public License */
/* along with this program. If not, see <http://www.gnu.org/licenses/>. */
/* */
/*******************************************************************************/
package com.github.shadowsocks
import android.os.Bundle
import android.support.v4.app.Fragment
import android.support.v7.widget.Toolbar
import android.view.View
class ToolbarFragment extends Fragment {
var toolbar: Toolbar = _
override def onViewCreated(view: View, savedInstanceState: Bundle) {
super.onViewCreated(view, savedInstanceState)
toolbar = view.findViewById[Toolbar](R.id.toolbar)
val activity = getActivity.asInstanceOf[MainActivity]
if (activity.crossfader == null) activity.drawer.setToolbar(activity, toolbar, true)
}
def onTrafficUpdated(profileId: Int, txRate: Long, rxRate: Long, txTotal: Long, rxTotal: Long): Unit = ()
def onBackPressed(): Boolean = false
}
package com.github.shadowsocks.acl
import java.io.{File, FileNotFoundException}
import java.net.URL
import android.util.Log
import com.github.shadowsocks.ShadowsocksApplication.app
import com.github.shadowsocks.utils.IOUtils
import com.j256.ormlite.field.DatabaseField
import scala.collection.mutable
import scala.io.Source
/**
* ACL handler compliant with: src/main/jni/shadowsocks-libev/src/acl.c
*
* OrmLite integration is unused for now.
*
* @author Mygod
*/
class Acl {
import Acl._
@DatabaseField(generatedId = true)
var id: Int = _
val bypassHostnames = new mutable.SortedList[String]()
val proxyHostnames = new mutable.SortedList[String]()
val subnets = new mutable.SortedList[Subnet]()
val urls = new mutable.SortedList[URL]()(urlOrdering)
@DatabaseField
var bypass: Boolean = _
def getBypassHostnamesString: String = bypassHostnames.mkString("\n")
def getProxyHostnamesString: String = proxyHostnames.mkString("\n")
def getSubnetsString: String = subnets.mkString("\n")
def getUrlsString: String = urls.mkString("\n")
def setBypassHostnamesString(value: String) {
bypassHostnames.clear()
bypassHostnames ++= value.split("\n")
}
def setProxyHostnamesString(value: String) {
proxyHostnames.clear()
proxyHostnames ++= value.split("\n")
}
def setSubnetsString(value: String) {
subnets.clear()
subnets ++= value.split("\n").map(Subnet.fromString)
}
def setUrlsString(value: String) {
urls.clear()
urls ++= value.split("\n").map(new URL(_))
}
def fromAcl(other: Acl): Acl = {
bypassHostnames.clear()
bypassHostnames ++= other.bypassHostnames
proxyHostnames.clear()
proxyHostnames ++= other.proxyHostnames
subnets.clear()
subnets ++= other.subnets
urls.clear()
urls ++= other.urls
bypass = other.bypass
this
}
def fromSource(value: Source, defaultBypass: Boolean = false): Acl = {
bypassHostnames.clear()
proxyHostnames.clear()
this.subnets.clear()
urls.clear()
bypass = defaultBypass
lazy val bypassSubnets = new mutable.SortedList[Subnet]()
lazy val proxySubnets = new mutable.SortedList[Subnet]()
var hostnames: mutable.SortedList[String] = if (defaultBypass) proxyHostnames else bypassHostnames
var subnets: mutable.SortedList[Subnet] = if (defaultBypass) proxySubnets else bypassSubnets
for (line <- value.getLines()) (line.indexOf('#') match {
case -1 => line
case index =>
line.substring(index + 1) match {
case networkAclParser(url) => urls.add(new URL(url))
case _ => // ignore
}
line.substring(0, index) // trim comments
}).trim match {
case "[outbound_block_list]" =>
hostnames = null
subnets = null
case "[black_list]" | "[bypass_list]" =>
hostnames = bypassHostnames
subnets = bypassSubnets
case "[white_list]" | "[proxy_list]" =>
hostnames = proxyHostnames
subnets = proxySubnets
case "[reject_all]" | "[bypass_all]" => bypass = true
case "[accept_all]" | "[proxy_all]" => bypass = false
case input if subnets != null && input.nonEmpty => try subnets += Subnet.fromString(input) catch {
case _: IllegalArgumentException => hostnames += input
}
case _ =>
}
this.subnets ++= (if (bypass) proxySubnets else bypassSubnets)
this
}
final def fromId(id: String): Acl = fromSource(Source.fromFile(Acl.getFile(id)))
def flatten(depth: Int): Acl = {
if (depth > 0) for (url <- urls) {
val child = new Acl().fromSource(Source.fromURL(url), bypass).flatten(depth - 1)
if (bypass != child.bypass) {
Log.w(TAG, "Imported network ACL has a conflicting mode set. This will probably not work as intended. URL: %s"
.format(url))
child.subnets.clear() // subnets for the different mode are discarded
child.bypass = bypass
}
bypassHostnames ++= child.bypassHostnames
proxyHostnames ++= child.proxyHostnames
subnets ++= child.subnets
}
urls.clear()
this
}
override def toString: String = {
val result = new StringBuilder()
result.append(if (bypass) "[bypass_all]\n" else "[proxy_all]\n")
val (bypassList, proxyList) =
if (bypass) (bypassHostnames.toStream, subnets.toStream.map(_.toString) #::: proxyHostnames.toStream)
else (subnets.toStream.map(_.toString) #::: bypassHostnames.toStream, proxyHostnames.toStream)
if (bypassList.nonEmpty) {
result.append("[bypass_list]\n")
result.append(bypassList.mkString("\n"))
result.append('\n')
}
if (proxyList.nonEmpty) {
result.append("[proxy_list]\n")
result.append(proxyList.mkString("\n"))
result.append('\n')
}
result.append(urls.map("#IMPORT_URL <%s>\n".format(_)).mkString)
result.toString
}
// Don't change: dummy fields for OrmLite interaction
// noinspection ScalaUnusedSymbol
@DatabaseField(useGetSet = true)
private val bypassHostnamesString: String = null
// noinspection ScalaUnusedSymbol
@DatabaseField(useGetSet = true)
private val proxyHostnamesString: String = null
// noinspection ScalaUnusedSymbol
@DatabaseField(useGetSet = true)
private val subnetsString: String = null
}
object Acl {
final val TAG = "Acl"
final val ALL = "all"
final val BYPASS_LAN = "bypass-lan"
final val BYPASS_CHN = "bypass-china"
final val BYPASS_LAN_CHN = "bypass-lan-china"
final val GFWLIST = "gfwlist"
final val CHINALIST = "china-list"
final val CUSTOM_RULES = "custom-rules"
final val CUSTOM_RULES_FLATTENED = "custom-rules-flattened"
private val networkAclParser = "^IMPORT_URL\\s*<(.+)>\\s*$".r
private val urlOrdering: Ordering[URL] = Ordering.by(url => (url.getHost, url.getPort, url.getFile, url.getProtocol))
def getFile(id: String) = new File(app.getFilesDir, id + ".acl")
def customRules: Acl = {
val acl = new Acl()
try acl.fromId(CUSTOM_RULES) catch {
case _: FileNotFoundException =>
}
acl.bypass = true
acl.bypassHostnames.clear() // everything is bypassed
acl
}
def save(id: String, acl: Acl): Unit = IOUtils.writeString(getFile(id), acl.toString)
}
/*******************************************************************************/
/* */
/* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* */
/* This program is free software: you can redistribute it and/or modify */
/* it under the terms of the GNU General Public License as published by */
/* the Free Software Foundation, either version 3 of the License, or */
/* (at your option) any later version. */
/* */
/* This program is distributed in the hope that it will be useful, */
/* but WITHOUT ANY WARRANTY; without even the implied warranty of */
/* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the */
/* GNU General Public License for more details. */
/* */
/* You should have received a copy of the GNU General Public License */
/* along with this program. If not, see <http://www.gnu.org/licenses/>. */
/* */
/*******************************************************************************/
package com.github.shadowsocks.acl
import java.io.IOException
import java.util.concurrent.TimeUnit
import com.evernote.android.job.Job.{Params, Result}
import com.evernote.android.job.{Job, JobRequest}
import com.github.shadowsocks.utils.IOUtils
import scala.io.Source
/**
* @author Mygod
*/
object AclSyncJob {
final val TAG = "AclSyncJob"
final val MAX_RESCHEDULE = 3
def schedule(route: String): Int = new JobRequest.Builder(AclSyncJob.TAG + ':' + route)
.setExecutionWindow(TimeUnit.SECONDS.toMillis(10), TimeUnit.DAYS.toMillis(28))
.setRequirementsEnforced(true)
.setRequiredNetworkType(JobRequest.NetworkType.UNMETERED)
.setRequiresCharging(true)
.setUpdateCurrent(true)
.build().schedule()
}
class AclSyncJob(route: String) extends Job {
override def onRunJob(params: Params): Result = {
try {
//noinspection JavaAccessorMethodCalledAsEmptyParen
IOUtils.writeString(Acl.getFile(route),
Source.fromURL("https://shadowsocks.org/acl/android/v1/" + route + ".acl").mkString)
Result.SUCCESS
} catch {
case e: IOException =>
e.printStackTrace()
if (params.getFailureCount < AclSyncJob.MAX_RESCHEDULE) Result.RESCHEDULE else Result.FAILURE
case e: Exception => // unknown failures, probably shouldn't retry
e.printStackTrace()
Result.FAILURE
}
}
}
package com.github.shadowsocks.acl
import java.net.{IDN, URL}
import java.util.Locale
import android.content.{ClipData, ClipboardManager, Context, DialogInterface}
import android.os.Bundle
import android.support.design.widget.Snackbar
import android.support.v7.app.AlertDialog
import android.support.v7.widget.RecyclerView.ViewHolder
import android.support.v7.widget.helper.ItemTouchHelper
import android.support.v7.widget.helper.ItemTouchHelper.SimpleCallback
import android.support.v7.widget.{DefaultItemAnimator, LinearLayoutManager, RecyclerView, Toolbar}
import android.view._
import android.widget.{EditText, Spinner, TextView, Toast}
import com.futuremind.recyclerviewfastscroll.{FastScroller, SectionTitleProvider}
import com.github.shadowsocks.ShadowsocksApplication.app
import com.github.shadowsocks.bg.ServiceState
import com.github.shadowsocks.widget.UndoSnackbarManager
import com.github.shadowsocks.{MainActivity, R, ToolbarFragment}
import scala.collection.mutable
import scala.io.Source
/**
* @author Mygod
*/
object CustomRulesFragment {
private final val TEMPLATE_GENERIC = 0
private final val TEMPLATE_DOMAIN = 1
private final val TEMPLATE_URL = 2
private val PATTERN_DOMAIN = """(?<=^\(\^\|\\\.\)).*(?=\$$)""".r
private final val TEMPLATE_REGEX_DOMAIN = "(^|\\.)%s$"
private final val SELECTED_SUBNETS = "com.github.shadowsocks.acl.CustomRulesFragment.SELECTED_SUBNETS"
private final val SELECTED_HOSTNAMES = "com.github.shadowsocks.acl.CustomRulesFragment.SELECTED_HOSTNAMES"
private final val SELECTED_URLS = "com.github.shadowsocks.acl.CustomRulesFragment.SELECTED_URLS"
}
class CustomRulesFragment extends ToolbarFragment with Toolbar.OnMenuItemClickListener {
import CustomRulesFragment._
private def isEnabled = getActivity.asInstanceOf[MainActivity].state match {
case ServiceState.CONNECTED => app.currentProfile.get.route != Acl.CUSTOM_RULES
case ServiceState.STOPPED => true
case _ => false
}
private def createAclRuleDialog(item: AnyRef = "") = {
val view = getActivity.getLayoutInflater.inflate(R.layout.dialog_acl_rule, null)
val templateSelector = view.findViewById[Spinner](R.id.template_selector)
val editText = view.findViewById[EditText](R.id.content)
item match {
case hostname: String => PATTERN_DOMAIN.findFirstMatchIn(hostname) match {
case Some(m) =>
templateSelector.setSelection(TEMPLATE_DOMAIN)
editText.setText(IDN.toUnicode(m.matched.replaceAll("\\\\.", "."),
IDN.ALLOW_UNASSIGNED | IDN.USE_STD3_ASCII_RULES))
case None =>
templateSelector.setSelection(TEMPLATE_GENERIC)
editText.setText(hostname)
}
case url: URL =>
templateSelector.setSelection(TEMPLATE_URL)
editText.setText(url.toString)
case i =>
templateSelector.setSelection(TEMPLATE_GENERIC)
editText.setText(i.toString)
}
(templateSelector, editText, new AlertDialog.Builder(getActivity)
.setTitle(R.string.edit_rule)
.setNegativeButton(android.R.string.cancel, null)
.setView(view))
}
private val selectedItems = new mutable.HashSet[AnyRef]
private def onSelectedItemsUpdated(): Unit =
if (selectionItem != null) selectionItem.setVisible(selectedItems.nonEmpty)
private final class AclRuleViewHolder(view: View) extends RecyclerView.ViewHolder(view)
with View.OnClickListener with View.OnLongClickListener {
var item: AnyRef = _
private val text = itemView.findViewById[TextView](android.R.id.text1)
itemView.setOnClickListener(this)
itemView.setOnLongClickListener(this)
itemView.setBackgroundResource(R.drawable.background_selectable)
def bind(hostname: String) {
item = hostname
text.setText(hostname)
itemView.setSelected(selectedItems.contains(hostname))
}
def bind(subnet: Subnet) {
item = subnet
text.setText(subnet.toString)
itemView.setSelected(selectedItems.contains(subnet))
}
def bind(url: URL) {
item = url
text.setText(url.toString)
itemView.setSelected(selectedItems.contains(url))
}
def onClick(v: View): Unit = if (selectedItems.nonEmpty) onLongClick(v) else {
val (templateSelector, editText, dialog) = createAclRuleDialog(item)
dialog
.setNeutralButton(R.string.delete, ((_, _) => {
adapter.remove(item)
undoManager.remove((-1, item))
}): DialogInterface.OnClickListener)
.setPositiveButton(android.R.string.ok, ((_, _) =>
adapter.addFromTemplate(templateSelector.getSelectedItemPosition, editText.getText) match {
case -1 =>
case index =>
val item = this.item
list.post(() => {
list.scrollToPosition(index)
adapter.remove(item)
})
})
: DialogInterface.OnClickListener)
.create().show()
}
def onLongClick(v: View): Boolean = {
if (!selectedItems.add(item)) selectedItems.remove(item) // toggle
onSelectedItemsUpdated()
itemView.setSelected(!itemView.isSelected)
true
}
}
private final class AclRulesAdapter extends RecyclerView.Adapter[AclRuleViewHolder] with SectionTitleProvider {
private val acl = Acl.customRules
private var savePending: Boolean = _
private def apply() = if (!savePending) {
savePending = true
list.post(() => {
Acl.save(Acl.CUSTOM_RULES, acl)
savePending = false
})
}
def getItemCount: Int = acl.subnets.size + acl.proxyHostnames.size + acl.urls.size
def onBindViewHolder(vh: AclRuleViewHolder, i: Int): Unit = {
val j = i - acl.subnets.size
if (j < 0) vh.bind(acl.subnets(i)) else {
val k = j - acl.proxyHostnames.size
if (k < 0) vh.bind(acl.proxyHostnames(j)) else vh.bind(acl.urls(k))
}
}
def onCreateViewHolder(vg: ViewGroup, i: Int) = new AclRuleViewHolder(LayoutInflater.from(vg.getContext)
.inflate(android.R.layout.simple_list_item_1, vg, false))
def addSubnet(subnet: Subnet): Int = if (acl.subnets.add(subnet)) {
val index = acl.subnets.indexOf(subnet)
notifyItemInserted(index)
apply()
index
} else -1
def addHostname(hostname: String): Int = if (acl.proxyHostnames.add(hostname)) {
val index = acl.proxyHostnames.indexOf(hostname) + acl.subnets.size
notifyItemInserted(index)
apply()
index
} else -1
def addURL(url: URL): Int = if (acl.urls.add(url)) {
val index = acl.urls.indexOf(url) + acl.subnets.size + acl.proxyHostnames.size
notifyItemInserted(index)
apply()
index
} else -1
def addToProxy(input: String): Int = {
val acl = new Acl().fromSource(Source.fromString(input), defaultBypass = true)
var result = -1
if (acl.bypass) for (subnet <- acl.subnets) result = addSubnet(subnet)
for (hostname <- acl.proxyHostnames) result = addHostname(hostname)
for (url <- acl.urls) result = addURL(url)
result
}
def addFromTemplate(template: Int, text: CharSequence): Int = template match {
case TEMPLATE_GENERIC => addToProxy(text.toString)
case TEMPLATE_DOMAIN => try addHostname(TEMPLATE_REGEX_DOMAIN.formatLocal(Locale.ENGLISH,
IDN.toASCII(text.toString, IDN.ALLOW_UNASSIGNED | IDN.USE_STD3_ASCII_RULES).replaceAll("\\.", "\\\\."))) catch {
case exc: IllegalArgumentException =>
Toast.makeText(getActivity, exc.getMessage, Toast.LENGTH_SHORT).show()
-1
}
case TEMPLATE_URL => addURL(new URL(text.toString))
case _ => -1
}
def remove(i: Int) {
val j = i - acl.subnets.size
if (j < 0) {
undoManager.remove((i, acl.subnets(i)))
acl.subnets.remove(i)
} else {
val k = j - acl.proxyHostnames.size
if (k < 0) {
undoManager.remove((j, acl.proxyHostnames(j)))
acl.proxyHostnames.remove(j)
} else {
undoManager.remove((k, acl.urls(k)))
acl.urls.remove(k)
}
}
notifyItemRemoved(i)
apply()
}
def remove(item: AnyRef): Unit = item match {
case subnet: Subnet =>
notifyItemRemoved(acl.subnets.indexOf(subnet))
acl.subnets.remove(subnet)
apply()
case hostname: String =>
notifyItemRemoved(acl.subnets.size + acl.proxyHostnames.indexOf(hostname))
acl.proxyHostnames.remove(hostname)
apply()
case url: URL =>
notifyItemRemoved(acl.subnets.size + acl.proxyHostnames.size + acl.urls.indexOf(url))
acl.urls.remove(url)
apply()
}
def removeSelected() {
undoManager.remove(selectedItems.map((-1, _)).toSeq: _*)
selectedItems.foreach(remove)
selectedItems.clear()
onSelectedItemsUpdated()
}
def undo(actions: Iterator[(Int, AnyRef)]): Unit = for ((_, item) <- actions) item match {
case subnet: Subnet => if (acl.subnets.insert(subnet)) {
notifyItemInserted(acl.subnets.indexOf(subnet))
apply()
}
case hostname: String => if (acl.proxyHostnames.insert(hostname)) {
notifyItemInserted(acl.subnets.size + acl.proxyHostnames.indexOf(hostname))
apply()
}
case url: URL => if (acl.urls.insert(url)) {
notifyItemInserted(acl.subnets.size + acl.proxyHostnames.size + acl.urls.indexOf(url))
apply()
}
}
def selectAll() {
selectedItems.clear()
selectedItems ++= acl.subnets
selectedItems ++= acl.proxyHostnames
selectedItems ++= acl.urls
onSelectedItemsUpdated()
notifyDataSetChanged()
}
def getSectionTitle(i: Int): String = {
val j = i - acl.subnets.size
try {
(if (j < 0) acl.subnets(i).address.getHostAddress.substring(0, 1) else {
val k = j - acl.proxyHostnames.size
if (k < 0) {
val hostname = acl.proxyHostnames(j)
PATTERN_DOMAIN.findFirstMatchIn(hostname) match {
case Some(m) => m.matched.replaceAll("\\\\.", ".") // don't convert IDN yet
case None => hostname
}
} else acl.urls(k).getHost
}).substring(0, 1)
} catch {
case _: IndexOutOfBoundsException => " "
}
}
}
private lazy val adapter = new AclRulesAdapter()
private var list: RecyclerView = _
private var selectionItem: MenuItem = _
private var undoManager: UndoSnackbarManager[AnyRef] = _
private lazy val clipboard = getActivity.getSystemService(Context.CLIPBOARD_SERVICE).asInstanceOf[ClipboardManager]
override def onCreateView(inflater: LayoutInflater, container: ViewGroup, savedInstanceState: Bundle): View =
inflater.inflate(R.layout.layout_custom_rules, container, false)
override def onViewCreated(view: View, savedInstanceState: Bundle) {
super.onViewCreated(view, savedInstanceState)
if (savedInstanceState != null) {
savedInstanceState.getStringArray(SELECTED_SUBNETS) match {
case null =>
case arr => selectedItems ++= arr.map(Subnet.fromString)
}
savedInstanceState.getStringArray(SELECTED_HOSTNAMES) match {
case null =>
case arr => selectedItems ++= arr
}
savedInstanceState.getStringArray(SELECTED_URLS) match {
case null =>
case arr => selectedItems ++= arr.map(new URL(_))
}
onSelectedItemsUpdated()
}
toolbar.setTitle(R.string.custom_rules)
toolbar.inflateMenu(R.menu.custom_rules_menu)
toolbar.setOnMenuItemClickListener(this)
selectionItem = toolbar.getMenu.findItem(R.id.selection)
selectionItem.setVisible(selectedItems.nonEmpty)
list = view.findViewById(R.id.list)
list.setLayoutManager(new LinearLayoutManager(getActivity, LinearLayoutManager.VERTICAL, false))
list.setItemAnimator(new DefaultItemAnimator)
list.setAdapter(adapter)
view.findViewById[FastScroller](R.id.fastscroller).setRecyclerView(list)
undoManager = new UndoSnackbarManager[AnyRef](getActivity.findViewById(R.id.snackbar), adapter.undo)
new ItemTouchHelper(new SimpleCallback(0, ItemTouchHelper.START | ItemTouchHelper.END) {
override def getSwipeDirs(recyclerView: RecyclerView, viewHolder: ViewHolder): Int =
if (isEnabled) super.getSwipeDirs(recyclerView, viewHolder) else 0
def onSwiped(viewHolder: ViewHolder, direction: Int): Unit = adapter.remove(viewHolder.getAdapterPosition)
def onMove(recyclerView: RecyclerView, viewHolder: ViewHolder, target: ViewHolder): Boolean = false
}).attachToRecyclerView(list)
}
override def onBackPressed(): Boolean = if (selectedItems.nonEmpty) {
selectedItems.clear()
onSelectedItemsUpdated()
adapter.notifyDataSetChanged()
true
} else super.onBackPressed()
override def onSaveInstanceState(outState: Bundle): Unit = {
super.onSaveInstanceState(outState)
outState.putStringArray(SELECTED_SUBNETS, selectedItems.filter(_.isInstanceOf[Subnet]).map(_.toString).toArray)
outState.putStringArray(SELECTED_HOSTNAMES, selectedItems.filter(_.isInstanceOf[String]).map(_.toString).toArray)
outState.putStringArray(SELECTED_URLS, selectedItems.filter(_.isInstanceOf[URL]).map(_.toString).toArray)
}
def copySelected() {
val acl = new Acl()
acl.bypass = true
selectedItems.foreach {
case subnet: Subnet => acl.subnets.add(subnet)
case hostname: String => acl.proxyHostnames.add(hostname)
case url: URL => acl.urls.add(url)
}
clipboard.setPrimaryClip(ClipData.newPlainText(null, acl.toString))
}
override def onMenuItemClick(menuItem: MenuItem): Boolean = menuItem.getItemId match {
case R.id.action_select_all =>
adapter.selectAll()
true
case R.id.action_cut =>
copySelected()
adapter.removeSelected()
true
case R.id.action_copy =>
copySelected()
true
case R.id.action_delete =>
adapter.removeSelected()
true
case R.id.action_manual_settings =>
val (templateSelector, editText, dialog) = createAclRuleDialog()
dialog
.setPositiveButton(android.R.string.ok, ((_, _) => adapter.addFromTemplate(
templateSelector.getSelectedItemPosition, editText.getText)): DialogInterface.OnClickListener)
.create().show()
true
case R.id.action_import =>
try adapter.addToProxy(clipboard.getPrimaryClip.getItemAt(0).getText.toString) != -1 catch {
case exc: Exception =>
Snackbar.make(getActivity.findViewById(R.id.snackbar), R.string.action_import_err, Snackbar.LENGTH_LONG)
.show()
exc.printStackTrace()
}
true
case R.id.action_import_gfwlist =>
val acl = new Acl().fromId(Acl.GFWLIST)
acl.subnets.foreach(adapter.addSubnet)
acl.proxyHostnames.foreach(adapter.addHostname)
acl.urls.foreach(adapter.addURL)
true
case _ => false
}
}
/*******************************************************************************/
/* */
/* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* */
/* This program is free software: you can redistribute it and/or modify */
/* it under the terms of the GNU General Public License as published by */
/* the Free Software Foundation, either version 3 of the License, or */
/* (at your option) any later version. */
/* */
/* This program is distributed in the hope that it will be useful, */
/* but WITHOUT ANY WARRANTY; without even the implied warranty of */
/* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the */
/* GNU General Public License for more details. */
/* */
/* You should have received a copy of the GNU General Public License */
/* along with this program. If not, see <http://www.gnu.org/licenses/>. */
/* */
/*******************************************************************************/
package com.github.shadowsocks.acl
import android.util.Log
import com.evernote.android.job.JobCreator
/**
* “I create jobs all day long.
* - Donald Trump, 2015
*
* Source: http://www.cnn.com/2015/09/24/politics/donald-trump-marco-rubio-foreign-policy/
*
* @author !Mygod
*/
object DonaldTrump extends JobCreator {
def create(tag: String): AclSyncJob = {
val parts = tag.split(":")
parts(0) match {
case AclSyncJob.TAG => new AclSyncJob(parts(1))
case _ =>
Log.w("DonaldTrump", "Unknown job tag: " + tag)
null
}
}
}
package com.github.shadowsocks.acl
import java.net.InetAddress
import java.util.Objects
import com.github.shadowsocks.utils.Utils
/**
* @author Mygod
*/
@throws[IllegalArgumentException]
class Subnet(val address: InetAddress, val prefixSize: Int) extends Comparable[Subnet] {
private def addressLength = address.getAddress.length << 3
if (prefixSize < 0 || prefixSize > addressLength) throw new IllegalArgumentException
override def toString: String =
if (prefixSize == addressLength) address.getHostAddress else address.getHostAddress + '/' + prefixSize
override def compareTo(that: Subnet): Int = {
val addrThis = address.getAddress
val addrThat = that.address.getAddress
var result = addrThis lengthCompare addrThat.length // IPv4 address goes first
if (result != 0) return result
for ((x, y) <- addrThis zip addrThat) {
result = (x & 0xFF) compare (y & 0xFF) // undo sign extension of signed byte
if (result != 0) return result
}
prefixSize compare that.prefixSize
}
override def equals(other: Any): Boolean = other match {
case that: Subnet => address == that.address && prefixSize == that.prefixSize
case _ => false
}
override def hashCode: Int = Objects.hash(address, prefixSize: Integer)
}
object Subnet {
@throws[IllegalArgumentException]
def fromString(value: String): Subnet = {
val parts = value.split("/")
val addr = Utils.parseNumericAddress(parts(0))
parts.length match {
case 1 => new Subnet(addr, addr.getAddress.length << 3)
case 2 => new Subnet(addr, parts(1).toInt)
case _ => throw new IllegalArgumentException()
}
}
}
/*******************************************************************************/
/* */
/* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* */
/* This program is free software: you can redistribute it and/or modify */
/* it under the terms of the GNU General Public License as published by */
/* the Free Software Foundation, either version 3 of the License, or */
/* (at your option) any later version. */
/* */
/* This program is distributed in the hope that it will be useful, */
/* but WITHOUT ANY WARRANTY; without even the implied warranty of */
/* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the */
/* GNU General Public License for more details. */
/* */
/* You should have received a copy of the GNU General Public License */
/* along with this program. If not, see <http://www.gnu.org/licenses/>. */
/* */
/*******************************************************************************/
package com.github.shadowsocks.bg
import java.io.{File, IOException}
import java.net.InetAddress
import java.util
import java.util.concurrent.TimeUnit
import java.util.{Timer, TimerTask}
import android.app.Service
import android.content._
import android.os.{Build, Handler, IBinder, RemoteCallbackList}
import android.text.TextUtils
import android.util.Log
import android.widget.Toast
import com.github.shadowsocks.ShadowsocksApplication.app
import com.github.shadowsocks.acl.{Acl, AclSyncJob}
import com.github.shadowsocks.aidl.{IShadowsocksService, IShadowsocksServiceCallback}
import com.github.shadowsocks.database.Profile
import com.github.shadowsocks.plugin.{PluginConfiguration, PluginManager, PluginOptions}
import com.github.shadowsocks.utils._
import com.github.shadowsocks.{GuardedProcess, R}
import okhttp3.{Dns, FormBody, OkHttpClient, Request}
import org.json.JSONObject
import scala.collection.mutable
import scala.collection.mutable.ArrayBuffer
import scala.util.Random
trait BaseService extends Service {
protected val TAG: String
@volatile private var state = ServiceState.STOPPED
@volatile protected var profile: Profile = _
@volatile private var plugin: PluginOptions = _
@volatile protected var pluginPath: String = _
var sslocalProcess: GuardedProcess = _
case class NameNotResolvedException() extends IOException
case class NullConnectionException() extends NullPointerException
var timer: Timer = _
var trafficMonitorThread: TrafficMonitorThread = _
final val callbacks = new RemoteCallbackList[IShadowsocksServiceCallback]
private final val bandwidthListeners = new mutable.HashSet[IBinder]() // the binder is the real identifier
lazy val handler = new Handler(getMainLooper)
lazy val restartHanlder = new Handler(getMainLooper)
private var notification: ServiceNotification = _
private val closeReceiver: BroadcastReceiver = (context: Context, intent: Intent) => intent.getAction match {
case Action.RELOAD => forceLoad()
case _ =>
Toast.makeText(context, R.string.stopping, Toast.LENGTH_SHORT).show()
stopRunner(stopService = true)
}
var closeReceiverRegistered: Boolean = _
val binder = new IShadowsocksService.Stub {
override def getState: Int = {
state
}
override def getProfileName: String = if (profile == null) null else profile.getName
override def registerCallback(cb: IShadowsocksServiceCallback): Unit = callbacks.register(cb)
override def startListeningForBandwidth(cb: IShadowsocksServiceCallback): Unit =
if (bandwidthListeners.add(cb.asBinder)){
if (timer == null) {
timer = new Timer(true)
timer.schedule(new TimerTask {
def run(): Unit = if (state == ServiceState.CONNECTED && TrafficMonitor.updateRate()) updateTrafficRate()
}, 1000, 1000)
}
TrafficMonitor.updateRate()
if (state == ServiceState.CONNECTED) cb.trafficUpdated(profile.id,
TrafficMonitor.txRate, TrafficMonitor.rxRate, TrafficMonitor.txTotal, TrafficMonitor.rxTotal)
}
override def stopListeningForBandwidth(cb: IShadowsocksServiceCallback): Unit =
if (bandwidthListeners.remove(cb.asBinder) && bandwidthListeners.isEmpty) {
timer.cancel()
timer = null
}
override def unregisterCallback(cb: IShadowsocksServiceCallback) {
stopListeningForBandwidth(cb) // saves an RPC, and safer
callbacks.unregister(cb)
}
}
def onBind(intent: Intent): IBinder = intent.getAction match {
case Action.SERVICE => binder
case _ => null
}
def checkProfile(profile: Profile): Boolean = if (TextUtils.isEmpty(profile.host) || TextUtils.isEmpty(profile.password)) {
stopRunner(stopService = true, getString(R.string.proxy_empty))
false
} else true
def forceLoad(): Unit = app.currentProfile.orNull match {
case null => stopRunner(stopService = true, getString(R.string.profile_empty))
case p => if (checkProfile(p)) state match {
case ServiceState.STOPPED => startRunner()
case ServiceState.CONNECTED =>
stopRunner(stopService = false)
startRunner()
case s => Log.w(BaseService.this.getClass.getSimpleName, "Illegal state when invoking use: " + s)
}
}
protected def buildAdditionalArguments(cmd: ArrayBuffer[String]): ArrayBuffer[String] = cmd
/**
* BaseService will only start ss-local. Child class override this class to start other native processes.
*/
def startNativeProcesses() {
buildShadowsocksConfig()
val cmd = buildAdditionalArguments(ArrayBuffer[String](
new File(getApplicationInfo.nativeLibraryDir, Executable.SS_LOCAL).getAbsolutePath,
"-u",
"-b", "127.0.0.1",
"-l", app.dataStore.portProxy.toString,
"-t", "600",
"-c", "shadowsocks.json"))
if (profile.route != Acl.ALL) {
cmd += "--acl"
cmd += Acl.getFile(profile.route match {
case Acl.CUSTOM_RULES => Acl.CUSTOM_RULES_FLATTENED
case route => route
}).getAbsolutePath
}
if (TcpFastOpen.sendEnabled) cmd += "--fast-open"
sslocalProcess = new GuardedProcess(cmd: _*).start()
}
def createNotification(): ServiceNotification
def startRunner(): Unit = if (Build.VERSION.SDK_INT >= 26) startForegroundService(new Intent(this, getClass))
else startService(new Intent(this, getClass))
def killProcesses() {
if (sslocalProcess != null) {
sslocalProcess.destroy()
sslocalProcess = null
}
}
def stopRunner(stopService: Boolean, msg: String = null) {
// channge the state
changeState(ServiceState.STOPPING)
app.track(TAG, "stop")
killProcesses()
// clean up recevier
if (closeReceiverRegistered) {
unregisterReceiver(closeReceiver)
closeReceiverRegistered = false
}
if (notification != null) notification.destroy()
// Make sure update total traffic when stopping the runner
updateTrafficTotal(TrafficMonitor.txTotal, TrafficMonitor.rxTotal)
TrafficMonitor.reset()
if (trafficMonitorThread != null) {
trafficMonitorThread.stopThread()
trafficMonitorThread = null
}
// change the state
changeState(ServiceState.STOPPED, msg)
// stop the service if nothing has bound to it
if (stopService) stopSelf()
profile = null
}
def updateTrafficTotal(tx: Long, rx: Long) {
val profile = this.profile // avoid race conditions without locking
if (profile != null) {
app.profileManager.getProfile(profile.id) match {
case Some(p) => // default profile may have host, etc. modified
p.tx += tx
p.rx += rx
app.profileManager.updateProfile(p)
handler.post(() => {
if (bandwidthListeners.nonEmpty) {
val n = callbacks.beginBroadcast()
for (i <- 0 until n) {
try {
val item = callbacks.getBroadcastItem(i)
if (bandwidthListeners.contains(item.asBinder)) item.trafficPersisted(p.id)
} catch {
case _: Exception => // Ignore
}
}
callbacks.finishBroadcast()
}
})
case None =>
}
}
}
def getState: Int = {
state
}
def updateTrafficRate() {
handler.post(() => {
if (bandwidthListeners.nonEmpty) {
val txRate = TrafficMonitor.txRate
val rxRate = TrafficMonitor.rxRate
val txTotal = TrafficMonitor.txTotal
val rxTotal = TrafficMonitor.rxTotal
val n = callbacks.beginBroadcast()
for (i <- 0 until n) {
try {
val item = callbacks.getBroadcastItem(i)
if (bandwidthListeners.contains(item.asBinder))
item.trafficUpdated(profile.id, txRate, rxRate, txTotal, rxTotal)
} catch {
case _: Exception => // Ignore
}
}
callbacks.finishBroadcast()
}
})
}
// Service of shadowsocks should always be started explicitly
override def onStartCommand(intent: Intent, flags: Int, startId: Int): Int = {
state match {
case ServiceState.STOPPED | ServiceState.IDLE =>
case _ => return Service.START_NOT_STICKY // ignore request
}
profile = app.currentProfile.orNull
if (profile == null) {
stopRunner(stopService = true)
return Service.START_NOT_STICKY
}
profile.name = profile.getName // save original name before it's (possibly) overwritten by IP addresses
TrafficMonitor.reset()
trafficMonitorThread = new TrafficMonitorThread(getApplicationContext)
trafficMonitorThread.start()
if (!closeReceiverRegistered) {
// register close receiver
val filter = new IntentFilter()
filter.addAction(Action.RELOAD)
filter.addAction(Intent.ACTION_SHUTDOWN)
filter.addAction(Action.CLOSE)
registerReceiver(closeReceiver, filter)
closeReceiverRegistered = true
}
notification = createNotification()
app.track(getClass.getSimpleName, "start")
changeState(ServiceState.CONNECTING)
Utils.ThrowableFuture(try {
if (profile.host == "198.199.101.152") {
val client = new OkHttpClient.Builder()
.dns(hostname => Utils.resolve(hostname, enableIPv6 = false) match {
case Some(ip) => util.Arrays.asList(InetAddress.getByName(ip))
case _ => Dns.SYSTEM.lookup(hostname)
})
.connectTimeout(10, TimeUnit.SECONDS)
.writeTimeout(10, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.build()
val requestBody = new FormBody.Builder()
.add("sig", Utils.getSignature(this))
.build()
val request = new Request.Builder()
.url(app.remoteConfig.getString("proxy_url"))
.post(requestBody)
.build()
val proxies = Random.shuffle(client.newCall(request).execute().body.string.split('|').toSeq)
val proxy = proxies.head.split(':')
profile.host = proxy(0).trim
profile.remotePort = proxy(1).trim.toInt
profile.password = proxy(2).trim
profile.method = proxy(3).trim
}
if (profile.route == Acl.CUSTOM_RULES) Acl.save(Acl.CUSTOM_RULES_FLATTENED, Acl.customRules.flatten(10))
plugin = new PluginConfiguration(profile.plugin).selectedOptions
pluginPath = PluginManager.init(plugin)
// Clean up
killProcesses()
if (!Utils.isNumeric(profile.host)) Utils.resolve(profile.host, enableIPv6 = true) match {
case Some(a) => profile.host = a
case None => throw NameNotResolvedException()
}
startNativeProcesses()
if (profile.route != Acl.ALL && profile.route != Acl.CUSTOM_RULES)
AclSyncJob.schedule(profile.route)
changeState(ServiceState.CONNECTED)
} catch {
case _: NameNotResolvedException => stopRunner(stopService = true, getString(R.string.invalid_server))
case _: NullConnectionException => stopRunner(stopService = true, getString(R.string.reboot_required))
case exc: Throwable =>
stopRunner(stopService = true, getString(R.string.service_failed) + ": " + exc.getMessage)
exc.printStackTrace()
app.track(exc)
})
Service.START_NOT_STICKY
}
protected def changeState(s: Int, msg: String = null) {
val handler = new Handler(getMainLooper)
if (state != s || msg != null) {
if (callbacks.getRegisteredCallbackCount > 0) handler.post(() => {
val n = callbacks.beginBroadcast()
for (i <- 0 until n) {
try {
callbacks.getBroadcastItem(i).stateChanged(s, binder.getProfileName, msg)
} catch {
case _: Exception => // Ignore
}
}
callbacks.finishBroadcast()
})
state = s
}
}
protected final def buildShadowsocksConfig() {
val config = new JSONObject()
.put("server", profile.host)
.put("server_port", profile.remotePort)
.put("password", profile.password)
.put("method", profile.method)
if (pluginPath != null) {
val pluginCmd = ArrayBuffer(pluginPath)
if (TcpFastOpen.sendEnabled) pluginCmd += "--fast-open"
config
.put("plugin", Commandline.toString(buildAdditionalArguments(pluginCmd).toArray))
.put("plugin_opts", plugin.toString)
}
IOUtils.writeString(new File(getFilesDir, "shadowsocks.json"), config.toString)
}
}
package com.github.shadowsocks.bg
import java.io.File
import java.util.Locale
import android.text.TextUtils
import android.util.Log
import com.github.shadowsocks.JniHelper
import com.github.shadowsocks.ShadowsocksApplication.app
import scala.io.Source
/**
* @author Mygod
*/
object Executable {
val REDSOCKS = "libredsocks.so"
val PDNSD = "libpdnsd.so"
val SS_LOCAL = "libss-local.so"
val SS_TUNNEL = "libss-tunnel.so"
val TUN2SOCKS = "libtun2socks.so"
val OVERTURE = "liboverture.so"
val EXECUTABLES = Set(SS_LOCAL, SS_TUNNEL, PDNSD, REDSOCKS, TUN2SOCKS, OVERTURE)
def killAll(): Unit =
for (process <- new File("/proc").listFiles((_, name) => TextUtils.isDigitsOnly(name))) {
val exe = new File(Source.fromFile(new File(process, "cmdline")).mkString.split("\0", 2).head)
if (exe.getParent == app.getApplicationInfo.nativeLibraryDir && EXECUTABLES.contains(exe.getName))
JniHelper.sigkill(process.getName.toInt) match {
case 0 =>
case errno => Log.w("kill", "SIGKILL %s (%d) failed with %d"
.formatLocal(Locale.ENGLISH, exe.getAbsolutePath, process.getName, errno))
}
}
}
package com.github.shadowsocks.bg
import java.io.File
import java.net.Inet6Address
import com.github.shadowsocks.GuardedProcess
import com.github.shadowsocks.ShadowsocksApplication.app
import com.github.shadowsocks.acl.Acl
import com.github.shadowsocks.utils.{IOUtils, Utils}
import org.json.{JSONArray, JSONObject}
import scala.collection.mutable.ArrayBuffer
/**
* Shadowsocks service with local DNS.
*
* @author Mygod
*/
trait LocalDnsService extends BaseService {
var overtureProcess: GuardedProcess = _
override def startNativeProcesses() {
super.startNativeProcesses()
if (!profile.udpdns) overtureProcess = new GuardedProcess(buildAdditionalArguments(ArrayBuffer[String](
new File(getApplicationInfo.nativeLibraryDir, Executable.OVERTURE).getAbsolutePath,
"-c", buildOvertureConfig("overture.conf")
)): _*).start()
}
private def buildOvertureConfig(file: String): String = {
val config = new JSONObject()
.put("BindAddress", "127.0.0.1:" + app.dataStore.portLocalDns)
.put("RedirectIPv6Record", true)
.put("DomainBase64Decode", true)
.put("HostsFile", "hosts")
.put("MinimumTTL", 3600)
.put("CacheSize", 4096)
def makeDns(name: String, address: String, edns: Boolean = true) = {
val dns = new JSONObject()
.put("Name", name)
.put("Address", (Utils.parseNumericAddress(address) match {
case _: Inet6Address => '[' + address + ']'
case _ => address
}) + ":53")
.put("Timeout", 6)
.put("EDNSClientSubnet", new JSONObject().put("Policy", "disable"))
if (edns) dns
.put("Protocol", "tcp")
.put("Socks5Address", "127.0.0.1:" + app.dataStore.portProxy)
else dns.put("Protocol", "udp")
dns
}
val remoteDns = new JSONArray(profile.remoteDns.split(",").zipWithIndex.map {
case (dns, i) => makeDns("UserDef-" + i, dns.trim)
})
val localDns = new JSONArray(Array(
makeDns("Primary-1", "119.29.29.29", edns = false),
makeDns("Primary-2", "114.114.114.114", edns = false)
))
try {
val localLinkDns = com.github.shadowsocks.utils.Dns.getDnsResolver(this)
localDns.put(makeDns("Primary-3", localLinkDns, edns = false))
} catch {
case _: Exception => // Ignore
}
profile.route match {
case Acl.BYPASS_CHN | Acl.BYPASS_LAN_CHN | Acl.GFWLIST | Acl.CUSTOM_RULES => config
.put("PrimaryDNS", localDns)
.put("AlternativeDNS", remoteDns)
.put("IPNetworkFile", "china_ip_list.txt")
.put("DomainFile", "gfwlist.txt")
case Acl.CHINALIST => config
.put("PrimaryDNS", localDns)
.put("AlternativeDNS", remoteDns)
case _ => config
.put("PrimaryDNS", remoteDns)
// no need to setup AlternativeDNS in Acl.ALL/BYPASS_LAN mode
.put("OnlyPrimaryDNS", true)
}
IOUtils.writeString(new File(getFilesDir, file), config.toString)
file
}
override def killProcesses() {
super.killProcesses()
if (overtureProcess != null) {
overtureProcess.destroy()
overtureProcess = null
}
}
}
package com.github.shadowsocks.bg
/**
* Shadowsocks service at its minimum.
*
* @author Mygod
*/
class ProxyService extends BaseService {
val TAG = "ShadowsocksProxyService"
def createNotification() = new ServiceNotification(this, profile.name, "service-proxy", true)
}
/*******************************************************************************/
/* */
/* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* */
/* This program is free software: you can redistribute it and/or modify */
/* it under the terms of the GNU General Public License as published by */
/* the Free Software Foundation, either version 3 of the License, or */
/* (at your option) any later version. */
/* */
/* This program is distributed in the hope that it will be useful, */
/* but WITHOUT ANY WARRANTY; without even the implied warranty of */
/* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the */
/* GNU General Public License for more details. */
/* */
/* You should have received a copy of the GNU General Public License */
/* along with this program. If not, see <http://www.gnu.org/licenses/>. */
/* */
/*******************************************************************************/
package com.github.shadowsocks.bg
import java.util.Locale
import android.app.{KeyguardManager, NotificationManager, PendingIntent}
import android.content.{BroadcastReceiver, Context, Intent, IntentFilter}
import android.os.{Build, PowerManager}
import android.support.v4.app.NotificationCompat
import android.support.v4.app.NotificationCompat.BigTextStyle
import android.support.v4.content.ContextCompat
import com.github.shadowsocks.aidl.IShadowsocksServiceCallback.Stub
import com.github.shadowsocks.utils.{Action, Utils}
import com.github.shadowsocks.{MainActivity, R}
/**
* @author Mygod
*/
class ServiceNotification(private val service: BaseService, profileName: String,
channel: String, visible: Boolean = false) {
private val keyGuard = service.getSystemService(Context.KEYGUARD_SERVICE).asInstanceOf[KeyguardManager]
private lazy val nm = service.getSystemService(Context.NOTIFICATION_SERVICE).asInstanceOf[NotificationManager]
private lazy val callback = new Stub {
override def stateChanged(state: Int, profileName: String, msg: String): Unit = () // ignore
override def trafficUpdated(profileId: Int, txRate: Long, rxRate: Long, txTotal: Long, rxTotal: Long) {
val txr = TrafficMonitor.formatTraffic(txRate)
val rxr = TrafficMonitor.formatTraffic(rxRate)
builder.setContentText(service.getString(R.string.traffic_summary).formatLocal(Locale.ENGLISH, txr, rxr))
style.bigText(service.getString(R.string.stat_summary).formatLocal(Locale.ENGLISH, txr, rxr,
TrafficMonitor.formatTraffic(txTotal), TrafficMonitor.formatTraffic(rxTotal)))
show()
}
override def trafficPersisted(profileId: Int): Unit = ()
}
private var lockReceiver: BroadcastReceiver = _
private var callbackRegistered: Boolean = _
private val builder = new NotificationCompat.Builder(service, channel)
.setWhen(0)
.setColor(ContextCompat.getColor(service, R.color.material_primary_500))
.setTicker(service.getString(R.string.forward_success))
.setContentTitle(profileName)
.setContentIntent(PendingIntent.getActivity(service, 0, new Intent(service, classOf[MainActivity])
.setFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT), 0))
.setSmallIcon(R.drawable.ic_stat_shadowsocks)
if (Build.VERSION.SDK_INT < 24) builder.addAction(R.drawable.ic_navigation_close,
service.getString(R.string.stop), PendingIntent.getBroadcast(service, 0, new Intent(Action.CLOSE), 0))
private lazy val style = new BigTextStyle(builder)
private var isVisible = true
update(if (service.getSystemService(Context.POWER_SERVICE).asInstanceOf[PowerManager].isScreenOn)
Intent.ACTION_SCREEN_ON else Intent.ACTION_SCREEN_OFF, forceShow = true)
lockReceiver = (_: Context, intent: Intent) => update(intent.getAction)
val screenFilter = new IntentFilter()
screenFilter.addAction(Intent.ACTION_SCREEN_ON)
screenFilter.addAction(Intent.ACTION_SCREEN_OFF)
if (visible && (21 until 26 contains Build.VERSION.SDK_INT)) screenFilter.addAction(Intent.ACTION_USER_PRESENT)
service.registerReceiver(lockReceiver, screenFilter)
private def update(action: String, forceShow: Boolean = false) =
if (forceShow || service.getState == ServiceState.CONNECTED) action match {
case Intent.ACTION_SCREEN_OFF =>
setVisible(visible && Build.VERSION.SDK_INT < 21, forceShow)
unregisterCallback() // unregister callback to save battery
case Intent.ACTION_SCREEN_ON =>
setVisible(visible && (Build.VERSION.SDK_INT < 21 || !keyGuard.inKeyguardRestrictedInputMode), forceShow)
service.binder.registerCallback(callback)
service.binder.startListeningForBandwidth(callback)
callbackRegistered = true
case Intent.ACTION_USER_PRESENT => setVisible(visible = true, forceShow = forceShow)
}
private def unregisterCallback() = if (callbackRegistered) {
service.binder.unregisterCallback(callback)
callbackRegistered = false
}
def setVisible(visible: Boolean, forceShow: Boolean = false): Unit = if (isVisible != visible) {
isVisible = visible
builder.setPriority(if (visible) NotificationCompat.PRIORITY_LOW else NotificationCompat.PRIORITY_MIN)
show()
} else if (forceShow) show()
def show(): Unit = service.startForeground(1, builder.build)
def destroy() {
if (lockReceiver != null) {
service.unregisterReceiver(lockReceiver)
lockReceiver = null
}
unregisterCallback()
service.stopForeground(true)
nm.cancel(1)
}
}
package com.github.shadowsocks.bg
/**
* @author Mygod
*/
object ServiceState {
/**
* This state will never be broadcast by the service. This state is only used to indicate that the current context
* hasn't bound to any context.
*/
val IDLE = 0
val CONNECTING = 1
val CONNECTED = 2
val STOPPING = 3
val STOPPED = 4
}
/*******************************************************************************/
/* */
/* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* */
/* This program is free software: you can redistribute it and/or modify */
/* it under the terms of the GNU General Public License as published by */
/* the Free Software Foundation, either version 3 of the License, or */
/* (at your option) any later version. */
/* */
/* This program is distributed in the hope that it will be useful, */
/* but WITHOUT ANY WARRANTY; without even the implied warranty of */
/* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the */
/* GNU General Public License for more details. */
/* */
/* You should have received a copy of the GNU General Public License */
/* along with this program. If not, see <http://www.gnu.org/licenses/>. */
/* */
/*******************************************************************************/
package com.github.shadowsocks.bg
import android.annotation.TargetApi
import android.graphics.drawable.Icon
import android.service.quicksettings.{Tile, TileService}
import com.github.shadowsocks.aidl.IShadowsocksServiceCallback
import com.github.shadowsocks.utils.Utils
import com.github.shadowsocks.{R, ServiceBoundContext}
/**
* @author Mygod
*/
@TargetApi(24)
final class ServiceTileService extends TileService with ServiceBoundContext {
private lazy val iconIdle = Icon.createWithResource(this, R.drawable.ic_start_idle).setTint(0x80ffffff)
private lazy val iconBusy = Icon.createWithResource(this, R.drawable.ic_start_busy)
private lazy val iconConnected = Icon.createWithResource(this, R.drawable.ic_start_connected)
private lazy val callback = new IShadowsocksServiceCallback.Stub {
def trafficUpdated(profileId: Int, txRate: Long, rxRate: Long, txTotal: Long, rxTotal: Long): Unit = ()
def stateChanged(state: Int, profileName: String, msg: String) {
val tile = getQsTile
if (tile != null) {
state match {
case ServiceState.STOPPED =>
tile.setIcon(iconIdle)
tile.setLabel(getString(R.string.app_name))
tile.setState(Tile.STATE_INACTIVE)
case ServiceState.CONNECTED =>
tile.setIcon(iconConnected)
tile.setLabel(if (profileName == null) getString(R.string.app_name) else profileName)
tile.setState(Tile.STATE_ACTIVE)
case _ =>
tile.setIcon(iconBusy)
tile.setLabel(getString(R.string.app_name))
tile.setState(Tile.STATE_UNAVAILABLE)
}
tile.updateTile()
}
}
override def trafficPersisted(profileId: Int): Unit = ()
}
override def onServiceConnected(): Unit = callback.stateChanged(bgService.getState, bgService.getProfileName, null)
override def onStartListening() {
super.onStartListening()
attachService(callback)
}
override def onStopListening() {
super.onStopListening()
detachService() // just in case the user switches to NAT mode, also saves battery
}
override def onClick(): Unit = if (isLocked) unlockAndRun(toggle) else toggle()
private def toggle() = if (bgService != null) bgService.getState match {
case ServiceState.STOPPED => Utils.startSsService(this)
case ServiceState.CONNECTED => Utils.stopSsService(this)
case _ => // ignore
}
}
/*******************************************************************************/
/* */
/* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* */
/* This program is free software: you can redistribute it and/or modify */
/* it under the terms of the GNU General Public License as published by */
/* the Free Software Foundation, either version 3 of the License, or */
/* (at your option) any later version. */
/* */
/* This program is distributed in the hope that it will be useful, */
/* but WITHOUT ANY WARRANTY; without even the implied warranty of */
/* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the */
/* GNU General Public License for more details. */
/* */
/* You should have received a copy of the GNU General Public License */
/* along with this program. If not, see <http://www.gnu.org/licenses/>. */
/* */
/*******************************************************************************/
package com.github.shadowsocks.bg
import java.text.DecimalFormat
import com.github.shadowsocks.R
import com.github.shadowsocks.ShadowsocksApplication.app
object TrafficMonitor {
// Bytes per second
var txRate: Long = _
var rxRate: Long = _
// Bytes for the current session
var txTotal: Long = _
var rxTotal: Long = _
// Bytes for the last query
var txLast: Long = _
var rxLast: Long = _
var timestampLast: Long = _
@volatile var dirty = true
private val units = Array("KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB", "BB", "NB", "DB", "CB")
private val numberFormat = new DecimalFormat("@@@")
def formatTraffic(size: Long): String = {
var n: Double = size
var i = -1
while (n >= 999.5) {
n /= 1024
i = i + 1
}
if (i < 0) size + " " + app.getResources.getQuantityString(R.plurals.bytes, size.toInt)
else numberFormat.format(n) + ' ' + units(i)
}
def updateRate(): Boolean = {
val now = System.currentTimeMillis()
val delta = now - timestampLast
var updated = false
if (delta != 0) {
if (dirty) {
txRate = (txTotal - txLast) * 1000 / delta
rxRate = (rxTotal - rxLast) * 1000 / delta
txLast = txTotal
rxLast = rxTotal
dirty = false
updated = true
} else {
if (txRate != 0) {
txRate = 0
updated = true
}
if (rxRate != 0) {
rxRate = 0
updated = true
}
}
timestampLast = now
}
updated
}
def update(tx: Long, rx: Long) {
if (txTotal != tx) {
txTotal = tx
dirty = true
}
if (rxTotal != rx) {
rxTotal = rx
dirty = true
}
}
def reset() {
txRate = 0
rxRate = 0
txTotal = 0
rxTotal = 0
txLast = 0
rxLast = 0
dirty = true
}
}
/*******************************************************************************/
/* */
/* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* */
/* This program is free software: you can redistribute it and/or modify */
/* it under the terms of the GNU General Public License as published by */
/* the Free Software Foundation, either version 3 of the License, or */
/* (at your option) any later version. */
/* */
/* This program is distributed in the hope that it will be useful, */
/* but WITHOUT ANY WARRANTY; without even the implied warranty of */
/* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the */
/* GNU General Public License for more details. */
/* */
/* You should have received a copy of the GNU General Public License */
/* along with this program. If not, see <http://www.gnu.org/licenses/>. */
/* */
/*******************************************************************************/
package com.github.shadowsocks.bg
import java.io.{File, IOException}
import java.nio.{ByteBuffer, ByteOrder}
import java.util.concurrent.Executors
import android.content.Context
import android.net.{LocalServerSocket, LocalSocket, LocalSocketAddress}
import android.util.Log
import com.github.shadowsocks.ShadowsocksApplication.app
class TrafficMonitorThread(context: Context) extends Thread {
val TAG = "TrafficMonitorThread"
val stat = new File(context.getFilesDir, "/stat_path")
@volatile var serverSocket: LocalServerSocket = _
@volatile var isRunning: Boolean = true
def closeServerSocket() {
if (serverSocket != null) {
try {
serverSocket.close()
} catch {
case _: Exception => // ignore
}
serverSocket = null
}
}
def stopThread() {
isRunning = false
closeServerSocket()
}
override def run() {
stat.delete()
try {
val localSocket = new LocalSocket
localSocket.bind(new LocalSocketAddress(stat.getAbsolutePath, LocalSocketAddress.Namespace.FILESYSTEM))
serverSocket = new LocalServerSocket(localSocket.getFileDescriptor)
} catch {
case e: IOException =>
Log.e(TAG, "unable to bind", e)
return
}
val pool = Executors.newFixedThreadPool(1)
while (isRunning) {
try {
val socket = serverSocket.accept()
pool.execute(() => {
try {
val input = socket.getInputStream
val output = socket.getOutputStream
val buffer = new Array[Byte](16)
if (input.read(buffer) != 16) throw new IOException("Unexpected traffic stat length")
val stat = ByteBuffer.wrap(buffer).order(ByteOrder.LITTLE_ENDIAN)
TrafficMonitor.update(stat.getLong(0), stat.getLong(8))
output.write(0)
input.close()
output.close()
} catch {
case e: Exception =>
Log.e(TAG, "Error when recv traffic stat", e)
app.track(e)
}
// close socket
try {
socket.close()
} catch {
case _: Exception => // ignore
}
})
} catch {
case e: IOException =>
Log.e(TAG, "Error when accept socket", e)
app.track(e)
return
}
}
}
}
/*******************************************************************************/
/* */
/* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* */
/* This program is free software: you can redistribute it and/or modify */
/* it under the terms of the GNU General Public License as published by */
/* the Free Software Foundation, either version 3 of the License, or */
/* (at your option) any later version. */
/* */
/* This program is distributed in the hope that it will be useful, */
/* but WITHOUT ANY WARRANTY; without even the implied warranty of */
/* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the */
/* GNU General Public License for more details. */
/* */
/* You should have received a copy of the GNU General Public License */
/* along with this program. If not, see <http://www.gnu.org/licenses/>. */
/* */
/*******************************************************************************/
package com.github.shadowsocks.bg
import java.io.File
import java.util.Locale
import com.github.shadowsocks.GuardedProcess
import com.github.shadowsocks.ShadowsocksApplication.app
import com.github.shadowsocks.utils.IOUtils
import scala.collection.mutable.ArrayBuffer
object TransproxyService {
private val REDSOCKS_CONFIG = "base {\n" +
" log_debug = off;\n" +
" log_info = off;\n" +
" log = stderr;\n" +
" daemon = off;\n" +
" redirector = iptables;\n" +
"}\n" +
"redsocks {\n" +
" local_ip = 127.0.0.1;\n" +
" local_port = %d;\n" +
" ip = 127.0.0.1;\n" +
" port = %d;\n" +
" type = socks5;\n" +
"}\n"
}
class TransproxyService extends LocalDnsService {
import TransproxyService._
val TAG = "ShadowsocksTransproxyService"
var sstunnelProcess: GuardedProcess = _
var redsocksProcess: GuardedProcess = _
def startDNSTunnel() {
val cmd = ArrayBuffer[String](new File(getApplicationInfo.nativeLibraryDir, Executable.SS_TUNNEL).getAbsolutePath,
"-t", "10",
"-b", "127.0.0.1",
"-u",
"-l", app.dataStore.portLocalDns.toString, // ss-tunnel listens on the same port as overture
"-L", profile.remoteDns.split(",").head.trim + ":53",
"-c", "shadowsocks.json") // config is already built by BaseService
sstunnelProcess = new GuardedProcess(cmd: _*).start()
}
def startRedsocksDaemon() {
IOUtils.writeString(new File(getFilesDir, "redsocks.conf"),
REDSOCKS_CONFIG.formatLocal(Locale.ENGLISH, app.dataStore.portTransproxy, app.dataStore.portProxy))
redsocksProcess = new GuardedProcess(
new File(getApplicationInfo.nativeLibraryDir, Executable.REDSOCKS).getAbsolutePath,
"-c", "redsocks.conf"
).start()
}
override def startNativeProcesses() {
startRedsocksDaemon()
super.startNativeProcesses()
if (profile.udpdns) startDNSTunnel()
}
override def killProcesses() {
super.killProcesses()
if (sstunnelProcess != null) {
sstunnelProcess.destroy()
sstunnelProcess = null
}
if (redsocksProcess != null) {
redsocksProcess.destroy()
redsocksProcess = null
}
}
def createNotification() = new ServiceNotification(this, profile.name, "service-transproxy", true)
}
/*******************************************************************************/
/* */
/* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* */
/* This program is free software: you can redistribute it and/or modify */
/* it under the terms of the GNU General Public License as published by */
/* the Free Software Foundation, either version 3 of the License, or */
/* (at your option) any later version. */
/* */
/* This program is distributed in the hope that it will be useful, */
/* but WITHOUT ANY WARRANTY; without even the implied warranty of */
/* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the */
/* GNU General Public License for more details. */
/* */
/* You should have received a copy of the GNU General Public License */
/* along with this program. If not, see <http://www.gnu.org/licenses/>. */
/* */
/*******************************************************************************/
package com.github.shadowsocks.bg
import java.io.File
import java.util.Locale
import android.app.Service
import android.content.Intent
import android.content.pm.PackageManager.NameNotFoundException
import android.net.{VpnService => BaseVpnService}
import android.os.{IBinder, ParcelFileDescriptor}
import android.util.Log
import com.github.shadowsocks.ShadowsocksApplication.app
import com.github.shadowsocks._
import com.github.shadowsocks.acl.{Acl, Subnet}
import com.github.shadowsocks.utils.Utils
import scala.collection.mutable.ArrayBuffer
class VpnService extends BaseVpnService with LocalDnsService {
val TAG = "ShadowsocksVpnService"
val VPN_MTU = 1500
val PRIVATE_VLAN = "26.26.26.%s"
val PRIVATE_VLAN6 = "fdfe:dcba:9876::%s"
var conn: ParcelFileDescriptor = _
var vpnThread: VpnThread = _
var tun2socksProcess: GuardedProcess = _
override def onBind(intent: Intent): IBinder = intent.getAction match {
case BaseVpnService.SERVICE_INTERFACE => super[VpnService].onBind(intent)
case _ => super[LocalDnsService].onBind(intent)
}
override def onRevoke() {
stopRunner(stopService = true)
}
override def killProcesses() {
if (vpnThread != null) {
vpnThread.stopThread()
vpnThread = null
}
super.killProcesses()
if (tun2socksProcess != null) {
tun2socksProcess.destroy()
tun2socksProcess = null
}
if (overtureProcess != null) {
overtureProcess.destroy()
overtureProcess = null
}
// close connections
if (conn != null) {
conn.close()
conn = null
}
}
override def onStartCommand(intent: Intent, flags: Int, startId: Int): Int = if (app.usingVpnMode)
// ensure the VPNService is prepared
if (BaseVpnService.prepare(this) != null) {
val i = new Intent(this, classOf[ShadowsocksRunnerActivity])
i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
startActivity(i)
stopRunner(stopService = true)
Service.START_NOT_STICKY
} else super.onStartCommand(intent, flags, startId)
else { // system or other apps are trying to start this service but other services could be running
stopSelf()
Service.START_NOT_STICKY
}
override def createNotification() = new ServiceNotification(this, profile.name, "service-vpn")
/** Called when the activity is first created. */
override def startNativeProcesses() {
vpnThread = new VpnThread(this)
vpnThread.start()
super.startNativeProcesses()
val fd = startVpn()
if (!sendFd(fd)) throw new Exception("sendFd failed")
}
override protected def buildAdditionalArguments(cmd: ArrayBuffer[String]): ArrayBuffer[String] = cmd += "-V"
def startVpn(): Int = {
val builder = new Builder()
builder
.setSession(profile.getName)
.setMtu(VPN_MTU)
.addAddress(PRIVATE_VLAN.formatLocal(Locale.ENGLISH, "1"), 24)
profile.remoteDns.split(",").foreach(dns => builder.addDnsServer(dns.trim))
if (profile.ipv6) {
builder.addAddress(PRIVATE_VLAN6.formatLocal(Locale.ENGLISH, "1"), 126)
builder.addRoute("::", 0)
}
if (Utils.isLollipopOrAbove) {
if (profile.proxyApps) {
for (pkg <- profile.individual.split('\n')) {
try {
if (!profile.bypass) {
builder.addAllowedApplication(pkg)
} else {
builder.addDisallowedApplication(pkg)
}
} catch {
case ex: NameNotFoundException =>
Log.e(TAG, "Invalid package name", ex)
}
}
}
}
if (profile.route == Acl.ALL
|| profile.route == Acl.BYPASS_CHN
|| profile.route == Acl.CUSTOM_RULES) {
builder.addRoute("0.0.0.0", 0)
} else {
getResources.getStringArray(R.array.bypass_private_route).foreach(cidr => {
val subnet = Subnet.fromString(cidr)
builder.addRoute(subnet.address.getHostAddress, subnet.prefixSize)
})
profile.remoteDns.split(",").map(dns => Utils.parseNumericAddress(dns.trim)).foreach(dns =>
builder.addRoute(dns, dns.getAddress.length << 3))
}
conn = builder.establish()
if (conn == null) throw new NullConnectionException
val fd = conn.getFd
var cmd = ArrayBuffer[String](new File(getApplicationInfo.nativeLibraryDir, Executable.TUN2SOCKS).getAbsolutePath,
"--netif-ipaddr", PRIVATE_VLAN.formatLocal(Locale.ENGLISH, "2"),
"--netif-netmask", "255.255.255.0",
"--socks-server-addr", "127.0.0.1:" + app.dataStore.portProxy,
"--tunfd", fd.toString,
"--tunmtu", VPN_MTU.toString,
"--sock-path", "sock_path",
"--loglevel", "3")
if (profile.ipv6)
cmd += ("--netif-ip6addr", PRIVATE_VLAN6.formatLocal(Locale.ENGLISH, "2"))
cmd += "--enable-udprelay"
if (!profile.udpdns)
cmd += ("--dnsgw", "%s:%d".formatLocal(Locale.ENGLISH, "127.0.0.1", app.dataStore.portLocalDns))
tun2socksProcess = new GuardedProcess(cmd: _*).start(() => sendFd(fd))
fd
}
def sendFd(fd: Int): Boolean = {
if (fd != -1) {
var tries = 1
while (tries < 5) {
Thread.sleep(1000 * tries)
if (JniHelper.sendFd(fd, new File(getFilesDir, "sock_path").getAbsolutePath) != -1) {
return true
}
tries += 1
}
}
false
}
}
/*******************************************************************************/
/* */
/* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* */
/* This program is free software: you can redistribute it and/or modify */
/* it under the terms of the GNU General Public License as published by */
/* the Free Software Foundation, either version 3 of the License, or */
/* (at your option) any later version. */
/* */
/* This program is distributed in the hope that it will be useful, */
/* but WITHOUT ANY WARRANTY; without even the implied warranty of */
/* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the */
/* GNU General Public License for more details. */
/* */
/* You should have received a copy of the GNU General Public License */
/* along with this program. If not, see <http://www.gnu.org/licenses/>. */
/* */
/*******************************************************************************/
package com.github.shadowsocks.bg
import java.io.{File, FileDescriptor, IOException}
import java.lang.reflect.Method
import java.util.concurrent.Executors
import android.net.{LocalServerSocket, LocalSocket, LocalSocketAddress}
import android.util.Log
import com.github.shadowsocks.JniHelper
import com.github.shadowsocks.ShadowsocksApplication.app
object VpnThread {
val getInt: Method = classOf[FileDescriptor].getDeclaredMethod("getInt$")
}
class VpnThread(service: VpnService) extends Thread {
import VpnThread._
val TAG = "ShadowsocksVpnService"
val protect = new File(service.getFilesDir, "protect_path")
@volatile var isRunning: Boolean = true
@volatile var serverSocket: LocalServerSocket = _
def closeServerSocket() {
if (serverSocket != null) {
try {
serverSocket.close()
} catch {
case _: Exception => // ignore
}
serverSocket = null
}
}
def stopThread() {
isRunning = false
closeServerSocket()
}
override def run() {
protect.delete()
try {
val localSocket = new LocalSocket
localSocket.bind(new LocalSocketAddress(protect.getAbsolutePath, LocalSocketAddress.Namespace.FILESYSTEM))
serverSocket = new LocalServerSocket(localSocket.getFileDescriptor)
} catch {
case e: IOException =>
Log.e(TAG, "unable to bind", e)
app.track(e)
return
}
val pool = Executors.newFixedThreadPool(4)
while (isRunning) {
try {
val socket = serverSocket.accept()
pool.execute(() => {
try {
val input = socket.getInputStream
val output = socket.getOutputStream
input.read()
val fds = socket.getAncillaryFileDescriptors
if (fds.nonEmpty) {
val fd = getInt.invoke(fds(0)).asInstanceOf[Int]
val ret = service.protect(fd)
// Trick to close file decriptor
JniHelper.close(fd)
if (ret) {
output.write(0)
} else {
output.write(1)
}
}
input.close()
output.close()
} catch {
case e: Exception =>
Log.e(TAG, "Error when protect socket", e)
app.track(e)
}
// close socket
try {
socket.close()
} catch {
case _: Exception => // ignore
}
})
} catch {
case e: IOException =>
Log.e(TAG, "Error when accept socket", e)
app.track(e)
return
}
}
}
}
/*******************************************************************************/
/* */
/* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* */
/* This program is free software: you can redistribute it and/or modify */
/* it under the terms of the GNU General Public License as published by */
/* the Free Software Foundation, either version 3 of the License, or */
/* (at your option) any later version. */
/* */
/* This program is distributed in the hope that it will be useful, */
/* but WITHOUT ANY WARRANTY; without even the implied warranty of */
/* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the */
/* GNU General Public License for more details. */
/* */
/* You should have received a copy of the GNU General Public License */
/* along with this program. If not, see <http://www.gnu.org/licenses/>. */
/* */
/*******************************************************************************/
package com.github.shadowsocks.database
import java.nio.ByteBuffer
import android.content.Context
import android.content.pm.ApplicationInfo
import android.database.sqlite.SQLiteDatabase
import android.preference.PreferenceManager
import com.github.shadowsocks.ShadowsocksApplication.app
import com.github.shadowsocks.utils.Key
import com.j256.ormlite.android.AndroidDatabaseConnection
import com.j256.ormlite.android.apptools.OrmLiteSqliteOpenHelper
import com.j256.ormlite.dao.Dao
import com.j256.ormlite.support.ConnectionSource
import com.j256.ormlite.table.TableUtils
import scala.collection.JavaConverters._
import scala.collection.mutable
object DBHelper {
final val PROFILE = "profile.db"
private var apps: mutable.Buffer[ApplicationInfo] = _
def isAllDigits(x: String): Boolean = !x.isEmpty && (x forall Character.isDigit)
def updateProxiedApps(context: Context, old: String): String = {
synchronized(if (apps == null) apps = context.getPackageManager.getInstalledApplications(0).asScala)
val uidSet = old.split('|').filter(isAllDigits).map(_.toInt).toSet
apps.filter(ai => uidSet.contains(ai.uid)).map(_.packageName).mkString("\n")
}
}
class DBHelper(val context: Context)
extends OrmLiteSqliteOpenHelper(context, DBHelper.PROFILE, null, 24) {
import DBHelper._
lazy val profileDao: Dao[Profile, Int] = getDao(classOf[Profile])
lazy val kvPairDao: Dao[KeyValuePair, String] = getDao(classOf[KeyValuePair])
def onCreate(database: SQLiteDatabase, connectionSource: ConnectionSource) {
TableUtils.createTable(connectionSource, classOf[Profile])
TableUtils.createTable(connectionSource, classOf[KeyValuePair])
}
def recreate(database: SQLiteDatabase, connectionSource: ConnectionSource) {
TableUtils.dropTable(connectionSource, classOf[Profile], true)
TableUtils.dropTable(connectionSource, classOf[KeyValuePair], true)
onCreate(database, connectionSource)
}
def onUpgrade(database: SQLiteDatabase, connectionSource: ConnectionSource, oldVersion: Int, newVersion: Int) {
if (oldVersion < 7) {
recreate(database, connectionSource)
return
}
try {
if (oldVersion < 8) {
profileDao.executeRawNoArgs("ALTER TABLE `profile` ADD COLUMN udpdns SMALLINT;")
}
if (oldVersion < 9) {
profileDao.executeRawNoArgs("ALTER TABLE `profile` ADD COLUMN route VARCHAR DEFAULT 'all';")
} else if (oldVersion < 19) {
profileDao.executeRawNoArgs("UPDATE `profile` SET route = 'all' WHERE route IS NULL;")
}
if (oldVersion < 11) {
profileDao.executeRawNoArgs("ALTER TABLE `profile` ADD COLUMN ipv6 SMALLINT;")
}
if (oldVersion < 12) {
profileDao.executeRawNoArgs("BEGIN TRANSACTION;")
profileDao.executeRawNoArgs("ALTER TABLE `profile` RENAME TO `tmp`;")
TableUtils.createTable(connectionSource, classOf[Profile])
profileDao.executeRawNoArgs(
"INSERT INTO `profile`(id, name, host, localPort, remotePort, password, method, route, proxyApps, bypass," +
" udpdns, ipv6, individual) " +
"SELECT id, name, host, localPort, remotePort, password, method, route, 1 - global, bypass, udpdns, ipv6," +
" individual FROM `tmp`;")
profileDao.executeRawNoArgs("DROP TABLE `tmp`;")
profileDao.executeRawNoArgs("COMMIT;")
} else if (oldVersion < 13) {
profileDao.executeRawNoArgs("ALTER TABLE `profile` ADD COLUMN tx LONG;")
profileDao.executeRawNoArgs("ALTER TABLE `profile` ADD COLUMN rx LONG;")
profileDao.executeRawNoArgs("ALTER TABLE `profile` ADD COLUMN date VARCHAR;")
}
if (oldVersion < 15) {
if (oldVersion >= 12) profileDao.executeRawNoArgs("ALTER TABLE `profile` ADD COLUMN userOrder LONG;")
var i = 0
for (profile <- profileDao.queryForAll.asScala) {
if (oldVersion < 14) profile.individual = updateProxiedApps(context, profile.individual)
profile.userOrder = i
profileDao.update(profile)
i += 1
}
}
if (oldVersion < 16) {
profileDao.executeRawNoArgs("UPDATE `profile` SET route = 'bypass-lan-china' WHERE route = 'bypass-china'")
}
if (oldVersion < 21) {
profileDao.executeRawNoArgs("ALTER TABLE `profile` ADD COLUMN remoteDns VARCHAR DEFAULT '8.8.8.8';")
}
if (oldVersion < 17) {
profileDao.executeRawNoArgs("ALTER TABLE `profile` ADD COLUMN plugin VARCHAR;")
} else if (oldVersion < 22) {
// upgrade kcptun to SIP003 plugin
profileDao.executeRawNoArgs("BEGIN TRANSACTION;")
profileDao.executeRawNoArgs("ALTER TABLE `profile` RENAME TO `tmp`;")
TableUtils.createTable(connectionSource, classOf[Profile])
profileDao.executeRawNoArgs(
"INSERT INTO `profile`(id, name, host, localPort, remotePort, password, method, route, remoteDns, " +
"proxyApps, bypass, udpdns, ipv6, individual, tx, rx, date, userOrder, plugin) " +
"SELECT id, name, host, localPort, CASE WHEN kcp = 1 THEN kcpPort ELSE remotePort END, password, method, " +
"route, remoteDns, proxyApps, bypass, udpdns, ipv6, individual, tx, rx, date, userOrder, " +
"CASE WHEN kcp = 1 THEN 'kcptun ' || kcpcli ELSE NULL END FROM `tmp`;")
profileDao.executeRawNoArgs("DROP TABLE `tmp`;")
profileDao.executeRawNoArgs("COMMIT;")
}
if (oldVersion < 23) {
profileDao.executeRawNoArgs("BEGIN TRANSACTION;")
TableUtils.createTable(connectionSource, classOf[KeyValuePair])
profileDao.executeRawNoArgs("COMMIT;")
import KeyValuePair._
val old = PreferenceManager.getDefaultSharedPreferences(app)
kvPairDao.createOrUpdate(new KeyValuePair(Key.id, TYPE_INT,
ByteBuffer.allocate(4).putInt(old.getInt(Key.id, 0)).array()))
kvPairDao.createOrUpdate(new KeyValuePair(Key.tfo, TYPE_BOOLEAN,
ByteBuffer.allocate(1).put((if (old.getBoolean(Key.tfo, false)) 1 else 0).toByte).array()))
}
} catch {
case ex: Exception =>
app.track(ex)
recreate(database, connectionSource)
}
}
override def onDowngrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
val connection = new AndroidDatabaseConnection(db, true)
connectionSource.saveSpecialConnection(connection)
recreate(db, connectionSource)
connectionSource.clearSpecialConnection(connection)
}
}
package com.github.shadowsocks.database
import com.j256.ormlite.field.{DataType, DatabaseField}
/**
* @author Mygod
*/
object KeyValuePair {
val TYPE_UNINITIALIZED = 0
val TYPE_BOOLEAN = 1
val TYPE_FLOAT = 2
val TYPE_INT = 3
val TYPE_LONG = 4
val TYPE_STRING = 5
val TYPE_STRING_SET = 6
}
class KeyValuePair {
@DatabaseField(id = true)
var key: String = _
@DatabaseField
var valueType: Int = _
@DatabaseField(dataType = DataType.BYTE_ARRAY)
var value: Array[Byte] = _
def this(key: String, valueType: Int, value: Array[Byte]) = {
this()
this.key = key
this.valueType = valueType
this.value = value
}
}
/*******************************************************************************/
/* */
/* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* */
/* This program is free software: you can redistribute it and/or modify */
/* it under the terms of the GNU General Public License as published by */
/* the Free Software Foundation, either version 3 of the License, or */
/* (at your option) any later version. */
/* */
/* This program is distributed in the hope that it will be useful, */
/* but WITHOUT ANY WARRANTY; without even the implied warranty of */
/* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the */
/* GNU General Public License for more details. */
/* */
/* You should have received a copy of the GNU General Public License */
/* along with this program. If not, see <http://www.gnu.org/licenses/>. */
/* */
/*******************************************************************************/
package com.github.shadowsocks.database
import java.util.Locale
import android.net.Uri
import android.util.Base64
import com.github.shadowsocks.plugin.PluginConfiguration
import com.github.shadowsocks.preference.OrmLitePreferenceDataStore
import com.github.shadowsocks.utils.{Key, Utils}
import com.j256.ormlite.field.{DataType, DatabaseField}
class Profile {
@DatabaseField(generatedId = true)
var id: Int = _
@DatabaseField
var name: String = ""
@DatabaseField
var host: String = "198.199.101.152"
@DatabaseField
var remotePort: Int = 8388
@DatabaseField
var password: String = "u1rRWTssNv0p"
@DatabaseField
var method: String = "aes-256-cfb"
@DatabaseField
var route: String = "all"
@DatabaseField
var remoteDns: String = "8.8.8.8"
@DatabaseField
var proxyApps: Boolean = _
@DatabaseField
var bypass: Boolean = _
@DatabaseField
var udpdns: Boolean = _
@DatabaseField
var ipv6: Boolean = _
@DatabaseField(dataType = DataType.LONG_STRING)
var individual: String = ""
@DatabaseField
var tx: Long = _
@DatabaseField
var rx: Long = _
@DatabaseField
val date: java.util.Date = new java.util.Date()
@DatabaseField
var userOrder: Long = _
@DatabaseField
var plugin: String = _
def formattedAddress: String = (if (host.contains(":")) "[%s]:%d" else "%s:%d").format(host, remotePort)
def nameIsEmpty: Boolean = name == null || name.isEmpty
def getName: String = if (nameIsEmpty) formattedAddress else name
def toUri: Uri = {
val builder = new Uri.Builder()
.scheme("ss")
.encodedAuthority("%s@%s:%d".formatLocal(Locale.ENGLISH,
Base64.encodeToString("%s:%s".formatLocal(Locale.ENGLISH, method, password).getBytes,
Base64.NO_PADDING | Base64.NO_WRAP | Base64.URL_SAFE),
if (host.contains(':')) '[' + host + ']' else host, remotePort))
val configuration = new PluginConfiguration(plugin)
if (configuration.selected.nonEmpty)
builder.appendQueryParameter(Key.plugin, configuration.selectedOptions.toString(false))
if (!nameIsEmpty) builder.fragment(name)
builder.build()
}
override def toString: String = toUri.toString
def serialize(store: OrmLitePreferenceDataStore) {
store.putString(Key.name, name)
store.putString(Key.host, host)
store.putString(Key.remotePort, remotePort.toString)
store.putString(Key.password, password)
store.putString(Key.route, route)
store.putString(Key.remoteDns, remoteDns)
store.putString(Key.method, method)
store.proxyApps = proxyApps
store.bypass = bypass
store.putBoolean(Key.udpdns, udpdns)
store.putBoolean(Key.ipv6, ipv6)
store.individual = individual
store.plugin = plugin
store.remove(Key.dirty)
}
def deserialize(store: OrmLitePreferenceDataStore) {
// It's assumed that default values are never used, so 0/false/null is always used even if that isn't the case
name = store.getString(Key.name, null)
host = store.getString(Key.host, null)
remotePort = Utils.parsePort(store.getString(Key.remotePort), 8388, 1)
password = store.getString(Key.password, null)
method = store.getString(Key.method, null)
route = store.getString(Key.route, null)
remoteDns = store.getString(Key.remoteDns, null)
proxyApps = store.proxyApps
bypass = store.bypass
udpdns = store.getBoolean(Key.udpdns, false)
ipv6 = store.getBoolean(Key.ipv6, false)
individual = store.individual
plugin = store.plugin
}
}
/*******************************************************************************/
/* */
/* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* */
/* This program is free software: you can redistribute it and/or modify */
/* it under the terms of the GNU General Public License as published by */
/* the Free Software Foundation, either version 3 of the License, or */
/* (at your option) any later version. */
/* */
/* This program is distributed in the hope that it will be useful, */
/* but WITHOUT ANY WARRANTY; without even the implied warranty of */
/* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the */
/* GNU General Public License for more details. */
/* */
/* You should have received a copy of the GNU General Public License */
/* along with this program. If not, see <http://www.gnu.org/licenses/>. */
/* */
/*******************************************************************************/
package com.github.shadowsocks.database
import android.util.Log
import com.github.shadowsocks.ProfilesFragment
import com.github.shadowsocks.ShadowsocksApplication.app
object ProfileManager {
private final val TAG = "ProfileManager"
}
class ProfileManager(dbHelper: DBHelper) {
import ProfileManager._
def createProfile(p: Profile = null): Profile = {
val profile = if (p == null) new Profile else p
profile.id = 0
try {
app.currentProfile match {
case Some(oldProfile) =>
// Copy Feature Settings from old profile
profile.route = oldProfile.route
profile.ipv6 = oldProfile.ipv6
profile.proxyApps = oldProfile.proxyApps
profile.bypass = oldProfile.bypass
profile.individual = oldProfile.individual
profile.udpdns = oldProfile.udpdns
case _ =>
}
val last = dbHelper.profileDao.queryRaw(dbHelper.profileDao.queryBuilder.selectRaw("MAX(userOrder)")
.prepareStatementString).getFirstResult
if (last != null && last.length == 1 && last(0) != null) profile.userOrder = last(0).toInt + 1
dbHelper.profileDao.createOrUpdate(profile)
if (ProfilesFragment.instance != null) ProfilesFragment.instance.profilesAdapter.add(profile)
} catch {
case ex: Exception =>
Log.e(TAG, "addProfile", ex)
app.track(ex)
}
profile
}
def updateProfile(profile: Profile): Boolean = {
try {
dbHelper.profileDao.update(profile)
true
} catch {
case ex: Exception =>
Log.e(TAG, "updateProfile", ex)
app.track(ex)
false
}
}
def getProfile(id: Int): Option[Profile] = {
try {
dbHelper.profileDao.queryForId(id) match {
case profile: Profile => Option(profile)
case _ => None
}
} catch {
case ex: Exception =>
Log.e(TAG, "getProfile", ex)
app.track(ex)
None
}
}
def delProfile(id: Int): Boolean = {
try {
dbHelper.profileDao.deleteById(id)
if (ProfilesFragment.instance != null) ProfilesFragment.instance.profilesAdapter.removeId(id)
true
} catch {
case ex: Exception =>
Log.e(TAG, "delProfile", ex)
app.track(ex)
false
}
}
def getFirstProfile: Option[Profile] = {
try {
val result = dbHelper.profileDao.query(dbHelper.profileDao.queryBuilder.limit(1L).prepare)
if (result.size == 1) Option(result.get(0)) else None
} catch {
case ex: Exception =>
Log.e(TAG, "getAllProfiles", ex)
app.track(ex)
None
}
}
def getAllProfiles: Option[List[Profile]] = {
try {
import scala.collection.JavaConversions._
Option(dbHelper.profileDao.query(dbHelper.profileDao.queryBuilder.orderBy("userOrder", true).prepare).toList)
} catch {
case ex: Exception =>
Log.e(TAG, "getAllProfiles", ex)
app.track(ex)
None
}
}
}
package com.github.shadowsocks.plugin
import android.content.pm.{PackageManager, ResolveInfo}
import android.os.Bundle
/**
* @author Mygod
*/
final class NativePlugin(resolveInfo: ResolveInfo, packageManager: PackageManager)
extends ResolvedPlugin(resolveInfo, packageManager) {
assert(resolveInfo.providerInfo != null)
override protected def metaData: Bundle = resolveInfo.providerInfo.metaData
override def packageName: String = resolveInfo.providerInfo.packageName
}
package com.github.shadowsocks.plugin
import com.github.shadowsocks.ShadowsocksApplication.app
/**
* @author Mygod
*/
object NoPlugin extends Plugin {
override def id: String = ""
override def label: CharSequence = app.getText(com.github.shadowsocks.R.string.plugin_disabled)
}
package com.github.shadowsocks.plugin
import android.graphics.drawable.Drawable
/**
* @author Mygod
*/
abstract class Plugin {
def id: String
def label: CharSequence
def icon: Drawable = null
def defaultConfig: String = null
def packageName: String = null
def trusted: Boolean = true
}
package com.github.shadowsocks.plugin
import android.util.Log
import com.github.shadowsocks.utils.Commandline
import scala.collection.mutable
/**
* @author Mygod
*/
class PluginConfiguration(val pluginsOptions: Map[String, PluginOptions], val selected: String) {
private def this(plugins: Array[PluginOptions]) =
this(plugins.filter(_.id.nonEmpty).map(opt => opt.id -> opt).toMap, if (plugins.isEmpty) "" else plugins(0).id)
def this(plugin: String) = this(if (plugin == null) Array[PluginOptions]() else plugin.split("\n").map {
case line if line.startsWith("kcptun ") =>
val opt = new PluginOptions()
opt.id = "kcptun"
try {
val args = mutable.Queue(Commandline.translateCommandline(line): _*)
args.dequeue()
while (args.nonEmpty) args.dequeue() match {
case "--nocomp" => opt.put("nocomp", null)
case option if option.startsWith("--") => opt.put(option.substring(2), args.dequeue())
case option => throw new IllegalArgumentException("Unknown kcptun parameter: " + option)
}
} catch {
case exc: Exception => Log.w("PluginConfiguration", exc.getMessage)
}
opt
case line => new PluginOptions(line)
})
def getOptions(id: String): PluginOptions = if (id.isEmpty) new PluginOptions() else pluginsOptions.get(id) match {
case Some(options) => options
case None => new PluginOptions(id, PluginManager.fetchPlugins()(id).defaultConfig)
}
def selectedOptions: PluginOptions = getOptions(selected)
override def toString: String = {
val result = new mutable.ListBuffer[PluginOptions]()
for ((id, opt) <- pluginsOptions) id match {
case this.selected => result.prepend(opt)
case _ => result.append(opt)
}
if (!pluginsOptions.contains(selected)) result.prepend(selectedOptions)
result.map(_.toString(false)).mkString("\n")
}
}
package com.github.shadowsocks.plugin
import java.io.{File, FileNotFoundException, FileOutputStream, IOException}
import android.content.pm.{PackageManager, Signature}
import android.content.{BroadcastReceiver, ContentResolver, Intent}
import android.net.Uri
import android.os.Bundle
import android.util.{Base64, Log}
import com.github.shadowsocks.ShadowsocksApplication.app
import com.github.shadowsocks.utils.CloseUtils.autoClose
import com.github.shadowsocks.utils.{Commandline, IOUtils}
import eu.chainfire.libsuperuser.Shell
import scala.collection.JavaConversions._
import scala.collection.mutable.ListBuffer
/**
* @author Mygod
*/
object PluginManager {
/**
* Trusted signatures by the app. Third-party fork should add their public key to their fork if the developer wishes
* to publish or has published plugins for this app. You can obtain your public key by executing:
*
* $ keytool -export -alias key-alias -keystore /path/to/keystore.jks -rfc
*
* If you don't plan to publish any plugin but is developing/has developed some, it's not necessary to add your
* public key yet since it will also automatically trust packages signed by the same signatures, e.g. debug keys.
*/
lazy val trustedSignatures: Set[Signature] = app.info.signatures.toSet +
new Signature(Base64.decode( // @Mygod
"""
|MIIDWzCCAkOgAwIBAgIEUzfv8DANBgkqhkiG9w0BAQsFADBdMQswCQYDVQQGEwJD
|TjEOMAwGA1UECBMFTXlnb2QxDjAMBgNVBAcTBU15Z29kMQ4wDAYDVQQKEwVNeWdv
|ZDEOMAwGA1UECxMFTXlnb2QxDjAMBgNVBAMTBU15Z29kMCAXDTE0MDUwMjA5MjQx
|OVoYDzMwMTMwOTAyMDkyNDE5WjBdMQswCQYDVQQGEwJDTjEOMAwGA1UECBMFTXln
|b2QxDjAMBgNVBAcTBU15Z29kMQ4wDAYDVQQKEwVNeWdvZDEOMAwGA1UECxMFTXln
|b2QxDjAMBgNVBAMTBU15Z29kMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC
|AQEAjm5ikHoP3w6zavvZU5bRo6Birz41JL/nZidpdww21q/G9APA+IiJMUeeocy0
|L7/QY8MQZABVwNq79LXYWJBcmmFXM9xBPgDqQP4uh9JsvazCI9bvDiMn92mz9HiS
|Sg9V4KGg0AcY0r230KIFo7hz+2QBp1gwAAE97myBfA3pi3IzJM2kWsh4LWkKQMfL
|M6KDhpb4mdDQnHlgi4JWe3SYbLtpB6whnTqjHaOzvyiLspx1tmrb0KVxssry9KoX
|YQzl56scfE/QJX0jJ5qYmNAYRCb4PibMuNSGB2NObDabSOMAdT4JLueOcHZ/x9tw
|agGQ9UdymVZYzf8uqc+29ppKdQIDAQABoyEwHzAdBgNVHQ4EFgQUBK4uJ0cqmnho
|6I72VmOVQMvVCXowDQYJKoZIhvcNAQELBQADggEBABZQ3yNESQdgNJg+NRIcpF9l
|YSKZvrBZ51gyrC7/2ZKMpRIyXruUOIrjuTR5eaONs1E4HI/uA3xG1eeW2pjPxDnO
|zgM4t7EPH6QbzibihoHw1MAB/mzECzY8r11PBhDQlst0a2hp+zUNR8CLbpmPPqTY
|RSo6EooQ7+NBejOXysqIF1q0BJs8Y5s/CaTOmgbL7uPCkzArB6SS/hzXgDk5gw6v
|wkGeOtzcj1DlbUTvt1s5GlnwBTGUmkbLx+YUje+n+IBgMbohLUDYBtUHylRVgMsc
|1WS67kDqeJiiQZvrxvyW6CZZ/MIGI+uAkkj3DqJpaZirkwPgvpcOIrjZy0uFvQM=
""", Base64.DEFAULT)) +
new Signature(Base64.decode( // @madeye
"""
|MIICQzCCAaygAwIBAgIETV9OhjANBgkqhkiG9w0BAQUFADBmMQswCQYDVQQGEwJjbjERMA8GA1UE
|CBMIU2hhbmdoYWkxDzANBgNVBAcTBlB1ZG9uZzEUMBIGA1UEChMLRnVkYW4gVW5pdi4xDDAKBgNV
|BAsTA1BQSTEPMA0GA1UEAxMGTWF4IEx2MB4XDTExMDIxOTA1MDA1NFoXDTM2MDIxMzA1MDA1NFow
|ZjELMAkGA1UEBhMCY24xETAPBgNVBAgTCFNoYW5naGFpMQ8wDQYDVQQHEwZQdWRvbmcxFDASBgNV
|BAoTC0Z1ZGFuIFVuaXYuMQwwCgYDVQQLEwNQUEkxDzANBgNVBAMTBk1heCBMdjCBnzANBgkqhkiG
|9w0BAQEFAAOBjQAwgYkCgYEAq6lA8LqdeEI+es9SDX85aIcx8LoL3cc//iRRi+2mFIWvzvZ+bLKr
|4Wd0rhu/iU7OeMm2GvySFyw/GdMh1bqh5nNPLiRxAlZxpaZxLOdRcxuvh5Nc5yzjM+QBv8ECmuvu
|AOvvT3UDmA0AMQjZqSCmxWIxc/cClZ/0DubreBo2st0CAwEAATANBgkqhkiG9w0BAQUFAAOBgQAQ
|Iqonxpwk2ay+Dm5RhFfZyG9SatM/JNFx2OdErU16WzuK1ItotXGVJaxCZv3u/tTwM5aaMACGED5n
|AvHaDGCWynY74oDAopM4liF/yLe1wmZDu6Zo/7fXrH+T03LBgj2fcIkUfN1AA4dvnBo8XWAm9VrI
|1iNuLIssdhDz3IL9Yg==
""", Base64.DEFAULT))
private var receiver: BroadcastReceiver = _
private var cachedPlugins: Map[String, Plugin] = _
def fetchPlugins(): Map[String, Plugin] = {
if (receiver == null) receiver = app.listenForPackageChanges(synchronized(cachedPlugins = null))
synchronized(if (cachedPlugins == null) {
val pm = app.getPackageManager
cachedPlugins = (
pm.queryIntentContentProviders(new Intent(PluginContract.ACTION_NATIVE_PLUGIN), PackageManager.GET_META_DATA)
.map(new NativePlugin(_, pm)) :+
NoPlugin
).map(plugin => plugin.id -> plugin).toMap
})
cachedPlugins
}
private def buildUri(id: String) = new Uri.Builder()
.scheme(PluginContract.SCHEME)
.authority(PluginContract.AUTHORITY)
.path('/' + id)
.build()
def buildIntent(id: String, action: String): Intent = new Intent(action, buildUri(id))
// the following parts are meant to be used by :bg
@throws[Throwable]
def init(options: PluginOptions): String = {
if (options.id.isEmpty) return null
var throwable: Throwable = null
try initNative(options) match {
case null =>
case path => return path
} catch {
case t: Throwable =>
t.printStackTrace()
if (throwable == null) throwable = t
}
// add other plugin types here
throw if (throwable == null) new FileNotFoundException(
app.getString(com.github.shadowsocks.R.string.plugin_unknown, options.id)) else throwable
}
@throws[IOException]
@throws[IndexOutOfBoundsException]
@throws[AssertionError]
private def initNative(options: PluginOptions): String = {
val providers = app.getPackageManager.queryIntentContentProviders(
new Intent(PluginContract.ACTION_NATIVE_PLUGIN, buildUri(options.id)), 0)
assert(providers.length == 1)
val uri = new Uri.Builder()
.scheme(ContentResolver.SCHEME_CONTENT)
.authority(providers(0).providerInfo.authority)
.build()
val cr = app.getContentResolver
try initNativeFast(cr, options, uri) catch {
case t: Throwable =>
t.printStackTrace()
Log.w("PluginManager", "Initializing native plugin fast mode failed. Falling back to slow mode.")
initNativeSlow(cr, options, uri)
}
}
private def initNativeFast(cr: ContentResolver, options: PluginOptions, uri: Uri): String = {
val out = new Bundle()
out.putString(PluginContract.EXTRA_OPTIONS, options.id)
val result = cr.call(uri, PluginContract.METHOD_GET_EXECUTABLE, null, out).getString(PluginContract.EXTRA_ENTRY)
assert(new File(result).canExecute)
result
}
private def initNativeSlow(cr: ContentResolver, options: PluginOptions, uri: Uri): String =
cr.query(uri, Array(PluginContract.COLUMN_PATH, PluginContract.COLUMN_MODE), null, null, null) match {
case null => null
case cursor =>
var initialized = false
val pluginDir = new File(app.getFilesDir, "plugin")
def entryNotFound() = throw new IndexOutOfBoundsException("Plugin entry binary not found")
if (!cursor.moveToFirst()) entryNotFound()
IOUtils.deleteRecursively(pluginDir)
if (!pluginDir.mkdirs()) throw new FileNotFoundException("Unable to create plugin directory")
val pluginDirPath = pluginDir.getAbsolutePath + '/'
val list = new ListBuffer[String]
do {
val path = cursor.getString(0)
val file = new File(pluginDir, path)
assert(file.getAbsolutePath.startsWith(pluginDirPath))
autoClose(cr.openInputStream(uri.buildUpon().path(path).build()))(in =>
autoClose(new FileOutputStream(file))(out => IOUtils.copy(in, out)))
list += Commandline.toString(Array("chmod", cursor.getString(1), file.getAbsolutePath))
if (path == options.id) initialized = true
} while (cursor.moveToNext())
if (!initialized) entryNotFound()
Shell.SH.run(list)
new File(pluginDir, options.id).getAbsolutePath
}
}
package com.github.shadowsocks.plugin
import android.content.pm.{PackageManager, ResolveInfo}
import android.graphics.drawable.Drawable
import android.os.Bundle
import com.github.shadowsocks.ShadowsocksApplication.app
/**
* Base class for any kind of plugin that can be used.
*
* @author Mygod
*/
abstract class ResolvedPlugin(resolveInfo: ResolveInfo, packageManager: PackageManager = app.getPackageManager)
extends Plugin {
protected def metaData: Bundle
override final lazy val id: String = metaData.getString(PluginContract.METADATA_KEY_ID)
override final lazy val label: CharSequence = resolveInfo.loadLabel(packageManager)
override final lazy val icon: Drawable = resolveInfo.loadIcon(packageManager)
override final lazy val defaultConfig: String = metaData.getString(PluginContract.METADATA_KEY_DEFAULT_CONFIG)
override def packageName: String = resolveInfo.resolvePackageName
override final lazy val trusted: Boolean = app.info.signatures.exists(PluginManager.trustedSignatures.contains)
}
package com.github.shadowsocks.preference
import android.content.{ActivityNotFoundException, Intent}
import android.graphics.Typeface
import android.graphics.drawable.Drawable
import android.net.Uri
import android.os.Bundle
import android.provider.Settings
import android.support.design.widget.BottomSheetDialog
import android.support.v7.preference.PreferenceDialogFragmentCompat
import android.support.v7.widget.RecyclerView.ViewHolder
import android.support.v7.widget.{LinearLayoutManager, RecyclerView}
import android.view.ViewGroup.LayoutParams
import android.view.{LayoutInflater, View, ViewGroup}
import android.widget.{ImageView, TextView}
import com.github.shadowsocks.R
/**
* @author Mygod
*/
final class BottomSheetPreferenceDialogFragment extends PreferenceDialogFragmentCompat {
private lazy val preference = getPreference.asInstanceOf[IconListPreference]
private lazy val index: Int = preference.selectedEntry
private lazy val entries: Array[CharSequence] = preference.getEntries
private lazy val entryValues: Array[CharSequence] = preference.getEntryValues
private lazy val entryIcons: Array[Drawable] = preference.getEntryIcons
private var clickedIndex = -1
override def onCreateDialog(savedInstanceState: Bundle): BottomSheetDialog = {
val activity = getActivity
val dialog = new BottomSheetDialog(activity, getTheme)
val recycler = new RecyclerView(activity)
recycler.setHasFixedSize(true)
recycler.setLayoutManager(new LinearLayoutManager(activity))
recycler.setAdapter(new IconListAdapter(dialog))
recycler.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT))
dialog.setContentView(recycler)
dialog
}
def onDialogClosed(positiveResult: Boolean): Unit = if (clickedIndex >= 0 && clickedIndex != index) {
val value = preference.getEntryValues()(clickedIndex).toString
if (preference.callChangeListener(value)) preference.setValue(value)
}
private final class IconListViewHolder(val dialog: BottomSheetDialog, view: View) extends ViewHolder(view)
with View.OnClickListener with View.OnLongClickListener {
private var index: Int = _
private val text1 = view.findViewById[TextView](android.R.id.text1)
private val text2 = view.findViewById[TextView](android.R.id.text2)
private val icon = view.findViewById[ImageView](android.R.id.icon)
view.setOnClickListener(this)
view.setOnLongClickListener(this)
def bind(i: Int, selected: Boolean = false) {
text1.setText(entries(i))
text2.setText(entryValues(i))
val typeface = if (selected) Typeface.BOLD else Typeface.NORMAL
text1.setTypeface(null, typeface)
text2.setTypeface(null, typeface)
text2.setVisibility(if (entryValues(i).length > 0 && entries(i) != entryValues(i)) View.VISIBLE else View.GONE)
icon.setImageDrawable(entryIcons(i))
index = i
}
def onClick(v: View) {
clickedIndex = index
dialog.dismiss()
}
override def onLongClick(v: View): Boolean = preference.entryPackageNames(index) match {
case null => false
case pn => try {
startActivity(new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, new Uri.Builder()
.scheme("package")
.opaquePart(pn)
.build()))
true
} catch {
case _: ActivityNotFoundException => false
}
}
}
private final class IconListAdapter(dialog: BottomSheetDialog) extends RecyclerView.Adapter[IconListViewHolder] {
def getItemCount: Int = entries.length
def onBindViewHolder(vh: IconListViewHolder, i: Int): Unit = if (index < 0) vh.bind(i) else i match {
case 0 => vh.bind(index, selected = true)
case _ if i > index => vh.bind(i)
case _ => vh.bind(i - 1)
}
def onCreateViewHolder(vg: ViewGroup, i: Int): IconListViewHolder = new IconListViewHolder(dialog,
LayoutInflater.from(vg.getContext).inflate(R.layout.icon_list_item_2, vg, false))
}
}
package com.github.shadowsocks.preference
import android.content.Context
import android.graphics.drawable.Drawable
import android.support.annotation.ArrayRes
import android.support.v7.preference.ListPreference
import android.support.v7.preference.Preference.OnPreferenceChangeListener
import android.util.AttributeSet
/**
* @author Mygod
*/
class IconListPreference(context: Context, attrs: AttributeSet = null) extends ListPreference(context, attrs) {
private var mEntryIcons: Array[Drawable] = _
def selectedEntry: Int = getEntryValues.indexOf(getValue)
var unknownValueSummary: String = _
var entryPackageNames: Array[String] = _
private var listener: OnPreferenceChangeListener = _
override def getOnPreferenceChangeListener: OnPreferenceChangeListener = listener
override def setOnPreferenceChangeListener(listener: OnPreferenceChangeListener): Unit = this.listener = listener
super.setOnPreferenceChangeListener((preference, newValue) =>
if (listener == null || listener.onPreferenceChange(preference, newValue)) {
setValue(newValue.toString)
checkSummary()
if (mEntryIcons != null) setIcon(getEntryIcon)
true
} else false)
def checkSummary(): Unit =
if (unknownValueSummary != null) setSummary(if (selectedEntry < 0) unknownValueSummary.format(getValue) else "%s")
// {
// val a: TypedArray = context.obtainStyledAttributes(attrs, R.styleable.IconListPreference)
// val entryIconsResId: Int = a.getResourceId(R.styleable.IconListPreference_entryIcons, -1)
// if (entryIconsResId != -1) setEntryIcons(entryIconsResId)
// a.recycle()
// }
def getEntryIcon: Drawable = try mEntryIcons(selectedEntry) catch {
case _: ArrayIndexOutOfBoundsException => null
}
def getEntryIcons: Array[Drawable] = mEntryIcons
def setEntryIcons(entryIcons: Array[Drawable]): Unit = mEntryIcons = entryIcons
def setEntryIcons(@ArrayRes entryIconsResId: Int) {
val icons_array = getContext.getResources.obtainTypedArray(entryIconsResId)
val icon_ids_array = new Array[Drawable](icons_array.length)
for (i <- 0 until icons_array.length) icon_ids_array(i) = icons_array.getDrawable(i)
setEntryIcons(icon_ids_array)
icons_array.recycle()
}
def init() {
val entryValues = getEntryValues
if (entryValues == null) return
if (mEntryIcons != null) setIcon(getEntryIcon)
}
protected override def onSetInitialValue(restoreValue: Boolean, defaultValue: AnyRef) {
super.onSetInitialValue(restoreValue, defaultValue)
init()
}
}
package com.github.shadowsocks.preference
import android.support.v7.preference.PreferenceDataStore
/**
* @author Mygod
*/
trait OnPreferenceDataStoreChangeListener {
def onPreferenceDataStoreChanged(store: PreferenceDataStore, key: String)
}
package com.github.shadowsocks.preference
import java.io.ByteArrayOutputStream
import java.nio.ByteBuffer
import java.util
import android.os.Binder
import android.support.v7.preference.PreferenceDataStore
import com.github.shadowsocks.database.{DBHelper, KeyValuePair}
import com.github.shadowsocks.utils.{Key, Utils}
import scala.collection.JavaConversions._
/**
* @author Mygod
*/
//noinspection AccessorLikeMethodIsUnit
final class OrmLitePreferenceDataStore(dbHelper: DBHelper) extends PreferenceDataStore {
import KeyValuePair._
override def getBoolean(key: String, defValue: Boolean = false): Boolean = dbHelper.kvPairDao.queryForId(key) match {
case pair: KeyValuePair =>
if (pair.valueType == TYPE_BOOLEAN) ByteBuffer.wrap(pair.value).get() != 0 else defValue
case _ => defValue
}
override def getFloat(key: String, defValue: Float): Float = dbHelper.kvPairDao.queryForId(key) match {
case pair: KeyValuePair =>
if (pair.valueType == TYPE_FLOAT) ByteBuffer.wrap(pair.value).getFloat() else defValue
case _ => defValue
}
override def getInt(key: String, defValue: Int): Int = dbHelper.kvPairDao.queryForId(key) match {
case pair: KeyValuePair =>
if (pair.valueType == TYPE_INT) ByteBuffer.wrap(pair.value).getInt() else defValue
case _ => defValue
}
override def getLong(key: String, defValue: Long): Long = dbHelper.kvPairDao.queryForId(key) match {
case pair: KeyValuePair =>
if (pair.valueType == TYPE_LONG) ByteBuffer.wrap(pair.value).getLong() else defValue
case _ => defValue
}
override def getString(key: String, defValue: String = null): String = dbHelper.kvPairDao.queryForId(key) match {
case pair: KeyValuePair =>
if (pair.valueType == TYPE_STRING) new String(pair.value) else defValue
case _ => defValue
}
override def getStringSet(key: String, defValue: util.Set[String]): util.Set[String] =
dbHelper.kvPairDao.queryForId(key) match {
case pair: KeyValuePair => if (pair.valueType == TYPE_STRING_SET) {
val buffer = ByteBuffer.wrap(pair.value)
val result = new util.HashSet[String]()
while (buffer.hasRemaining) {
val chArr = new Array[Byte](buffer.getInt)
buffer.get(chArr)
result.add(new String(chArr))
}
result
} else defValue
case _ => defValue
}
override def putBoolean(key: String, value: Boolean) {
dbHelper.kvPairDao.createOrUpdate(
new KeyValuePair(key, TYPE_BOOLEAN, ByteBuffer.allocate(1).put((if (value) 1 else 0).toByte).array()))
fireChangeListener(key)
}
override def putFloat(key: String, value: Float) {
dbHelper.kvPairDao.createOrUpdate(new KeyValuePair(key, TYPE_FLOAT, ByteBuffer.allocate(4).putFloat(value).array()))
fireChangeListener(key)
}
override def putInt(key: String, value: Int) {
dbHelper.kvPairDao.createOrUpdate(new KeyValuePair(key, TYPE_INT, ByteBuffer.allocate(4).putInt(value).array()))
fireChangeListener(key)
}
override def putLong(key: String, value: Long) {
dbHelper.kvPairDao.createOrUpdate(new KeyValuePair(key, TYPE_LONG, ByteBuffer.allocate(8).putLong(value).array()))
fireChangeListener(key)
}
override def putString(key: String, value: String) {
value match {
case null => remove(key)
case _ => dbHelper.kvPairDao.createOrUpdate(new KeyValuePair(key, TYPE_STRING, value.getBytes()))
}
fireChangeListener(key)
}
override def putStringSet(key: String, value: util.Set[String]) {
val stream = new ByteArrayOutputStream()
for (v <- value) {
stream.write(ByteBuffer.allocate(4).putInt(v.length).array())
stream.write(v.getBytes())
}
dbHelper.kvPairDao.createOrUpdate(new KeyValuePair(key, TYPE_STRING_SET, stream.toByteArray))
fireChangeListener(key)
}
def remove(key: String): Int = dbHelper.kvPairDao.deleteById(key)
private var listeners: Set[OnPreferenceDataStoreChangeListener] = Set.empty
private def fireChangeListener(key: String) = listeners.foreach(_.onPreferenceDataStoreChanged(this, key))
def registerChangeListener(listener: OnPreferenceDataStoreChangeListener): Unit = listeners += listener
def unregisterChangeListener(listener: OnPreferenceDataStoreChangeListener): Unit = listeners -= listener
// hopefully hashCode = mHandle doesn't change, currently this is true from KitKat to Nougat
private lazy val userIndex = Binder.getCallingUserHandle.hashCode
private def getLocalPort(key: String, default: Int) = getInt(key, 0) match {
case 0 => Utils.parsePort(getString(key), default + userIndex)
case value =>
putString(key, value.toString)
value
}
def profileId: Int = getInt(Key.id, 0)
def profileId_=(i: Int): Unit = putInt(Key.id, i)
def serviceMode: String = getString(Key.serviceMode, Key.modeVpn)
def portProxy: Int = getLocalPort(Key.portProxy, 1080)
def portLocalDns: Int = getLocalPort(Key.portLocalDns, 5450)
def portTransproxy: Int = getLocalPort(Key.portTransproxy, 8200)
def proxyApps: Boolean = getBoolean(Key.proxyApps)
def proxyApps_=(value: Boolean): Unit = putBoolean(Key.proxyApps, value)
def bypass: Boolean = getBoolean(Key.bypass)
def bypass_=(value: Boolean): Unit = putBoolean(Key.bypass, value)
def individual: String = getString(Key.individual)
def individual_=(value: String): Unit = putString(Key.individual, value)
def plugin: String = getString(Key.plugin)
def plugin_=(value: String): Unit = putString(Key.plugin, value)
def dirty: Boolean = getBoolean(Key.dirty)
def dirty_=(value: Boolean): Unit = putBoolean(Key.dirty, value)
def initGlobal() {
// temporary workaround for support lib bug
if (getString(Key.serviceMode) == null) putString(Key.serviceMode, serviceMode)
if (getString(Key.portProxy) == null) putString(Key.portProxy, portProxy.toString)
if (getString(Key.portLocalDns) == null) putString(Key.portLocalDns, portLocalDns.toString)
if (getString(Key.portTransproxy) == null) putString(Key.portTransproxy, portTransproxy.toString)
}
}
/*******************************************************************************/
/* */
/* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* */
/* This program is free software: you can redistribute it and/or modify */
/* it under the terms of the GNU General Public License as published by */
/* the Free Software Foundation, either version 3 of the License, or */
/* (at your option) any later version. */
/* */
/* This program is distributed in the hope that it will be useful, */
/* but WITHOUT ANY WARRANTY; without even the implied warranty of */
/* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the */
/* GNU General Public License for more details. */
/* */
/* You should have received a copy of the GNU General Public License */
/* along with this program. If not, see <http://www.gnu.org/licenses/>. */
/* */
/*******************************************************************************/
package com.github.shadowsocks.preference
import android.content.DialogInterface
import android.support.v7.app.AlertDialog
import android.support.v7.preference.EditTextPreferenceDialogFragmentCompat
import android.view.View
import android.widget.EditText
import com.github.shadowsocks.ProfileConfigActivity
import com.github.shadowsocks.plugin.{PluginContract, PluginManager}
/**
* @author Mygod
*/
object PluginConfigurationDialogFragment {
final val PLUGIN_ID_FRAGMENT_TAG = "com.github.shadowsocks.preference.PluginConfigurationDialogFragment.PLUGIN_ID"
}
class PluginConfigurationDialogFragment extends EditTextPreferenceDialogFragmentCompat {
import PluginConfigurationDialogFragment._
private var editText: EditText = _
override def onPrepareDialogBuilder(builder: AlertDialog.Builder) {
super.onPrepareDialogBuilder(builder)
val intent = PluginManager.buildIntent(getArguments.getString(PLUGIN_ID_FRAGMENT_TAG), PluginContract.ACTION_HELP)
if (intent.resolveActivity(getActivity.getPackageManager) != null) builder.setNeutralButton("?", ((_, _) =>
getActivity.startActivityForResult(intent.putExtra(PluginContract.EXTRA_OPTIONS, editText.getText.toString),
ProfileConfigActivity.REQUEST_CODE_PLUGIN_HELP)): DialogInterface.OnClickListener)
}
override def onBindDialogView(view: View) {
super.onBindDialogView(view)
editText = view.findViewById(android.R.id.edit)
}
}
/*******************************************************************************/
/* */
/* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* */
/* This program is free software: you can redistribute it and/or modify */
/* it under the terms of the GNU General Public License as published by */
/* the Free Software Foundation, either version 3 of the License, or */
/* (at your option) any later version. */
/* */
/* This program is distributed in the hope that it will be useful, */
/* but WITHOUT ANY WARRANTY; without even the implied warranty of */
/* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the */
/* GNU General Public License for more details. */
/* */
/* You should have received a copy of the GNU General Public License */
/* along with this program. If not, see <http://www.gnu.org/licenses/>. */
/* */
/*******************************************************************************/
package com.github.shadowsocks.utils
/**
* @author Mygod
*/
object CloseUtils {
type Disconnectable = {
def disconnect()
}
def autoClose[A <: AutoCloseable, B](x: => A)(block: A => B): B = {
var a: Option[A] = None
try {
a = Some(x)
block(a.get)
} finally if (a.nonEmpty) try {
val v = a.get
if (v ne null) v.close()
} catch {
case _: Exception =>
}
}
def autoDisconnect[A <: Disconnectable, B](x: => A)(block: A => B): B = {
var a: Option[A] = None
try {
a = Some(x)
block(a.get)
} finally if (a.nonEmpty) try {
val v = a.get
if (v ne null) v.disconnect()
} catch {
case _: Exception =>
}
}
}
/*******************************************************************************/
/* */
/* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* */
/* This program is free software: you can redistribute it and/or modify */
/* it under the terms of the GNU General Public License as published by */
/* the Free Software Foundation, either version 3 of the License, or */
/* (at your option) any later version. */
/* */
/* This program is distributed in the hope that it will be useful, */
/* but WITHOUT ANY WARRANTY; without even the implied warranty of */
/* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the */
/* GNU General Public License for more details. */
/* */
/* You should have received a copy of the GNU General Public License */
/* along with this program. If not, see <http://www.gnu.org/licenses/>. */
/* */
/*******************************************************************************/
package com.github.shadowsocks.utils
object Key {
val id = "profileId"
val name = "profileName"
val individual = "Proxyed"
val serviceMode = "serviceMode"
val modeProxy = "proxy"
val modeVpn = "vpn"
val modeTransproxy = "transproxy"
val portProxy = "portProxy"
val portLocalDns = "portLocalDns"
val portTransproxy = "portTransproxy"
val route = "route"
val isAutoConnect = "isAutoConnect"
val proxyApps = "isProxyApps"
val bypass = "isBypassApps"
val udpdns = "isUdpDns"
val ipv6 = "isIpv6"
val host = "proxy"
val password = "sitekey"
val method = "encMethod"
val remotePort = "remotePortNum"
val remoteDns = "remoteDns"
val plugin = "plugin"
val pluginConfigure = "plugin.configure"
val dirty = "profileDirty"
val tfo = "tcp_fastopen"
val assetUpdateTime = "assetUpdateTime"
}
object Action {
final val SERVICE = "com.github.shadowsocks.SERVICE"
final val CLOSE = "com.github.shadowsocks.CLOSE"
final val RELOAD = "com.github.shadowsocks.RELOAD"
final val EXTRA_PROFILE_ID = "com.github.shadowsocks.EXTRA_PROFILE_ID"
}
/*******************************************************************************/
/* */
/* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* */
/* This program is free software: you can redistribute it and/or modify */
/* it under the terms of the GNU General Public License as published by */
/* the Free Software Foundation, either version 3 of the License, or */
/* (at your option) any later version. */
/* */
/* This program is distributed in the hope that it will be useful, */
/* but WITHOUT ANY WARRANTY; without even the implied warranty of */
/* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the */
/* GNU General Public License for more details. */
/* */
/* You should have received a copy of the GNU General Public License */
/* along with this program. If not, see <http://www.gnu.org/licenses/>. */
/* */
/*******************************************************************************/
package com.github.shadowsocks.utils
import com.github.shadowsocks.utils.CloseUtils._
import java.io._
/**
* @author Mygod
*/
object IOUtils {
private final val BUFFER_SIZE = 32 * 1024
def copy(in: InputStream, out: OutputStream) {
val buffer = new Array[Byte](BUFFER_SIZE)
while (true) {
val count = in.read(buffer)
if (count >= 0) out.write(buffer, 0, count) else return
}
}
def writeString(file: File, content: String): Unit =
autoClose(new FileWriter(file))(writer => writer.write(content))
def writeString(file: String, content: String): Unit =
autoClose(new FileWriter(file))(writer => writer.write(content))
@throws[FileNotFoundException]
def deleteRecursively(file: File): Unit = if (file.exists()) {
if (file.isDirectory) file.listFiles().foreach(deleteRecursively)
if (!file.delete()) throw new FileNotFoundException("Failed to delete: " + file.getAbsolutePath)
}
}
/*******************************************************************************/
/* */
/* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* */
/* This program is free software: you can redistribute it and/or modify */
/* it under the terms of the GNU General Public License as published by */
/* the Free Software Foundation, either version 3 of the License, or */
/* (at your option) any later version. */
/* */
/* This program is distributed in the hope that it will be useful, */
/* but WITHOUT ANY WARRANTY; without even the implied warranty of */
/* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the */
/* GNU General Public License for more details. */
/* */
/* You should have received a copy of the GNU General Public License */
/* along with this program. If not, see <http://www.gnu.org/licenses/>. */
/* */
/*******************************************************************************/
package com.github.shadowsocks.utils
import java.net.URI
import android.net.Uri
import android.util.{Base64, Log}
import com.github.shadowsocks.database.Profile
object Parser {
val TAG = "ShadowParser"
private val pattern = "(?i)ss://[-a-zA-Z0-9+&@#/%?=~_|!:,.;\\[\\]]*[-a-zA-Z0-9+&@#/%=~_|\\[\\]]".r
private val userInfoPattern = "^(.+?):(.*)$".r
private val legacyPattern = "^(.+?):(.*)@(.+?):(\\d+?)$".r
def findAll(data: CharSequence): Iterator[Profile] =
pattern.findAllMatchIn(if (data == null) "" else data).map(m => {
val uri = Uri.parse(m.matched)
uri.getUserInfo match {
case null => val str = new String(Base64.decode(uri.getHost, Base64.NO_PADDING), "UTF-8"); str match {
case legacyPattern(method, password, host, port) => // legacy uri
val profile = new Profile
profile.method = method.toLowerCase
profile.password = password
profile.host = host
profile.remotePort = port.toInt
profile.plugin = uri.getQueryParameter(Key.plugin)
profile.name = uri.getFragment
profile
case _ =>
Log.e(TAG, "Unrecognized URI: " + m.matched)
null
}
case userInfo =>
new String(Base64.decode(userInfo, Base64.NO_PADDING | Base64.NO_WRAP | Base64.URL_SAFE), "UTF-8") match {
case userInfoPattern(method, password) =>
val profile = new Profile
profile.method = method
profile.password = password
// bug in Android: https://code.google.com/p/android/issues/detail?id=192855
val javaURI = new URI(m.matched)
profile.host = javaURI.getHost
if (profile.host.headOption.contains('[') && profile.host.lastOption.contains(']'))
profile.host = profile.host.substring(1, profile.host.length - 1)
profile.remotePort = javaURI.getPort
profile.plugin = uri.getQueryParameter(Key.plugin)
profile.name = uri.getFragment
profile
case _ =>
Log.e(TAG, "Unknown user info: " + m.matched)
null
}
}
}).filter(_ != null)
}
/*******************************************************************************/
/* */
/* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* */
/* This program is free software: you can redistribute it and/or modify */
/* it under the terms of the GNU General Public License as published by */
/* the Free Software Foundation, either version 3 of the License, or */
/* (at your option) any later version. */
/* */
/* This program is distributed in the hope that it will be useful, */
/* but WITHOUT ANY WARRANTY; without even the implied warranty of */
/* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the */
/* GNU General Public License for more details. */
/* */
/* You should have received a copy of the GNU General Public License */
/* along with this program. If not, see <http://www.gnu.org/licenses/>. */
/* */
/*******************************************************************************/
package com.github.shadowsocks.utils
import android.content.{Context, Intent}
import android.os.Bundle
import com.github.shadowsocks.R
import com.github.shadowsocks.ShadowsocksApplication.app
import com.twofortyfouram.locale.api.{Intent => ApiIntent}
object TaskerSettings {
private val KEY_SWITCH_ON = "switch_on"
private val KEY_PROFILE_ID = "profile_id"
def fromIntent(intent: Intent) = new TaskerSettings(intent.getBundleExtra(ApiIntent.EXTRA_BUNDLE))
}
class TaskerSettings(bundle: Bundle) {
import TaskerSettings._
var switchOn: Boolean = bundle.getBoolean(KEY_SWITCH_ON, true)
var profileId: Int = bundle.getInt(KEY_PROFILE_ID, -1)
def toIntent(context: Context): Intent = {
val bundle = new Bundle()
if (!switchOn) bundle.putBoolean(KEY_SWITCH_ON, false)
if (profileId >= 0) bundle.putInt(KEY_PROFILE_ID, profileId)
new Intent().putExtra(ApiIntent.EXTRA_BUNDLE, bundle).putExtra(ApiIntent.EXTRA_STRING_BLURB,
app.profileManager.getProfile(profileId) match {
case Some(p) => context.getString(if (switchOn) R.string.start_service else R.string.stop_service, p.getName)
case None => context.getString(if (switchOn) R.string.start_service_default else R.string.stop)
})
}
}
/*******************************************************************************/
/* */
/* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* */
/* This program is free software: you can redistribute it and/or modify */
/* it under the terms of the GNU General Public License as published by */
/* the Free Software Foundation, either version 3 of the License, or */
/* (at your option) any later version. */
/* */
/* This program is distributed in the hope that it will be useful, */
/* but WITHOUT ANY WARRANTY; without even the implied warranty of */
/* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the */
/* GNU General Public License for more details. */
/* */
/* You should have received a copy of the GNU General Public License */
/* along with this program. If not, see <http://www.gnu.org/licenses/>. */
/* */
/*******************************************************************************/
package com.github.shadowsocks.utils
import java.io.File
import eu.chainfire.libsuperuser.Shell
import scala.collection.JavaConverters._
import scala.io.Source
/**
* @author Mygod
*/
object TcpFastOpen {
/**
* Is kernel version >= 3.7.1.
*/
lazy val supported: Boolean = "^(\\d+)\\.(\\d+)\\.(\\d+)".r.findFirstMatchIn(System.getProperty("os.version")) match {
case Some(m) =>
val kernel = m.group(1).toInt
if (kernel < 3) false else if (kernel > 3) true else {
val major = m.group(2).toInt
if (major < 7) false else if (major > 7) true else m.group(3).toInt >= 1
}
case _ => false
}
def sendEnabled: Boolean = {
val file = new File("/proc/sys/net/ipv4/tcp_fastopen")
file.canRead && (Source.fromFile(file).mkString.trim.toInt & 1) > 0
}
def enabled(value: Boolean): String = if (sendEnabled != value) {
val res = Shell.run("su", Array(
"if echo " + (if (value) 3 else 0) + " > /proc/sys/net/ipv4/tcp_fastopen; then",
" echo Success.",
"else",
" echo Failed.",
"fi"), null, true)
if (res != null) res.asScala.mkString("\n") else null
} else null
}
/*******************************************************************************/
/* */
/* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* */
/* This program is free software: you can redistribute it and/or modify */
/* it under the terms of the GNU General Public License as published by */
/* the Free Software Foundation, either version 3 of the License, or */
/* (at your option) any later version. */
/* */
/* This program is distributed in the hope that it will be useful, */
/* but WITHOUT ANY WARRANTY; without even the implied warranty of */
/* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the */
/* GNU General Public License for more details. */
/* */
/* You should have received a copy of the GNU General Public License */
/* along with this program. If not, see <http://www.gnu.org/licenses/>. */
/* */
/*******************************************************************************/
package com.github.shadowsocks.utils
import java.util
import android.content.Context
import android.graphics.Typeface
import android.util.Log
import com.github.shadowsocks.ShadowsocksApplication.app
object Typefaces {
def get(c: Context, assetPath: String): Typeface = {
cache synchronized {
if (!cache.containsKey(assetPath)) {
try {
cache.put(assetPath, Typeface.createFromAsset(c.getAssets, assetPath))
} catch {
case e: Exception =>
Log.e("Typefaces", "Could not get typeface '" + assetPath + "' because " + e.getMessage)
app.track(e)
return null
}
}
return cache.get(assetPath)
}
}
private final val cache = new util.Hashtable[String, Typeface]
}
/*******************************************************************************/
/* */
/* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* */
/* This program is free software: you can redistribute it and/or modify */
/* it under the terms of the GNU General Public License as published by */
/* the Free Software Foundation, either version 3 of the License, or */
/* (at your option) any later version. */
/* */
/* This program is distributed in the hope that it will be useful, */
/* but WITHOUT ANY WARRANTY; without even the implied warranty of */
/* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the */
/* GNU General Public License for more details. */
/* */
/* You should have received a copy of the GNU General Public License */
/* along with this program. If not, see <http://www.gnu.org/licenses/>. */
/* */
/*******************************************************************************/
package com.github.shadowsocks.utils
import java.lang.reflect.InvocationTargetException
import java.net._
import java.security.MessageDigest
import android.content.pm.PackageManager
import android.content.{Context, Intent}
import android.graphics._
import android.os.Build
import android.util.{Base64, DisplayMetrics, Log}
import android.view.View.MeasureSpec
import android.view.{Gravity, View, Window}
import android.widget.Toast
import com.github.shadowsocks.ShadowsocksApplication.app
import com.github.shadowsocks.bg.{ProxyService, TransproxyService, VpnService}
import com.github.shadowsocks.BuildConfig
import org.xbill.DNS._
import scala.collection.JavaConversions._
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.Future
import scala.util.{Failure, Try}
object Utils {
private val TAG = "Shadowsocks"
def isLollipopOrAbove: Boolean = Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP
def getSignature(context: Context): String = {
val mdg = MessageDigest.getInstance("SHA-1")
mdg.update(app.info.signatures(0).toByteArray)
new String(Base64.encode(mdg.digest, 0))
}
/*
* round or floor depending on whether you are using offsets(floor) or
* widths(round)
*/
// Based on: http://stackoverflow.com/a/21026866/2245107
def positionToast(toast: Toast, view: View, window: Window, offsetX: Int = 0, offsetY: Int = 0): Toast = {
val rect = new Rect
window.getDecorView.getWindowVisibleDisplayFrame(rect)
val viewLocation = new Array[Int](2)
view.getLocationInWindow(viewLocation)
val metrics = new DisplayMetrics
window.getWindowManager.getDefaultDisplay.getMetrics(metrics)
val toastView = toast.getView
toastView.measure(MeasureSpec.makeMeasureSpec(metrics.widthPixels, MeasureSpec.UNSPECIFIED),
MeasureSpec.makeMeasureSpec(metrics.heightPixels, MeasureSpec.UNSPECIFIED))
toast.setGravity(Gravity.LEFT | Gravity.TOP,
viewLocation(0) - rect.left + (view.getWidth - toast.getView.getMeasuredWidth) / 2 + offsetX,
viewLocation(1) - rect.top + view.getHeight + offsetY)
toast
}
def resolve(host: String, addrType: Int): Option[String] = {
try {
val lookup = new Lookup(host, addrType)
val resolver = new SimpleResolver("208.67.220.220")
resolver.setTCP(true)
resolver.setPort(443)
resolver.setTimeout(5)
lookup.setResolver(resolver)
val result = lookup.run()
if (result == null) return None
val records = scala.util.Random.shuffle(result.toList)
for (r <- records) {
addrType match {
case Type.A =>
return Some(r.asInstanceOf[ARecord].getAddress.getHostAddress)
case Type.AAAA =>
return Some(r.asInstanceOf[AAAARecord].getAddress.getHostAddress)
}
}
} catch {
case _: Exception =>
}
None
}
def resolve(host: String): Option[String] = try Some(InetAddress.getByName(host).getHostAddress) catch {
case _: UnknownHostException => None
}
def resolve(host: String, enableIPv6: Boolean): Option[String] =
(if (enableIPv6 && isIPv6Support) resolve(host, Type.AAAA) else None).orElse(resolve(host, Type.A))
.orElse(resolve(host))
private lazy val isNumericMethod = classOf[InetAddress].getMethod("isNumeric", classOf[String])
private lazy val parseNumericAddressMethod = classOf[InetAddress].getMethod("parseNumericAddress", classOf[String])
def isNumeric(address: String): Boolean = isNumericMethod.invoke(null, address).asInstanceOf[Boolean]
def parseNumericAddress(address: String): InetAddress =
try parseNumericAddressMethod.invoke(null, address).asInstanceOf[InetAddress] catch {
case exc: InvocationTargetException => throw exc.getCause match {
case null => exc
case cause => cause
}
}
/**
* If there exists a valid IPv6 interface
*/
def isIPv6Support: Boolean = try {
val result = NetworkInterface.getNetworkInterfaces.flatMap(_.getInetAddresses)
.count(addr => addr.isInstanceOf[Inet6Address] && !addr.isLoopbackAddress && !addr.isLinkLocalAddress) > 0
if (result && BuildConfig.DEBUG) Log.d(TAG, "IPv6 address detected")
result
} catch {
case ex: Exception =>
Log.e(TAG, "Failed to get interfaces' addresses.", ex)
app.track(ex)
false
}
def startSsService(context: Context) {
val intent = new Intent(context, app.serviceClass)
if (Build.VERSION.SDK_INT >= 26) context.startForegroundService(intent) else context.startService(intent)
}
def reloadSsService(context: Context): Unit = context.sendBroadcast(new Intent(Action.RELOAD))
def stopSsService(context: Context) {
val intent = new Intent(Action.CLOSE)
context.sendBroadcast(intent)
}
private val handleFailure: Try[_] => Unit = {
case Failure(e) =>
e.printStackTrace()
app.track(e)
case _ =>
}
def ThrowableFuture[T](f: => T): Unit = Future(f) onComplete handleFailure
def parsePort(str: String, default: Int, min: Int = 1025): Int = (try str.toInt catch {
case _: NumberFormatException => default
}) match {
case x if x < min => default
case x if x > 65535 => default
case x => x
}
}
/*******************************************************************************/
/* */
/* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* */
/* This program is free software: you can redistribute it and/or modify */
/* it under the terms of the GNU General Public License as published by */
/* the Free Software Foundation, either version 3 of the License, or */
/* (at your option) any later version. */
/* */
/* This program is distributed in the hope that it will be useful, */
/* but WITHOUT ANY WARRANTY; without even the implied warranty of */
/* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the */
/* GNU General Public License for more details. */
/* */
/* You should have received a copy of the GNU General Public License */
/* along with this program. If not, see <http://www.gnu.org/licenses/>. */
/* */
/*******************************************************************************/
package com.github.shadowsocks.widget
import android.content.Context
import android.support.v7.widget.GridLayout
import android.util.AttributeSet
/**
* @author Mygod
*/
class BoundedGridLayout(context: Context, attrs: AttributeSet) extends GridLayout(context, attrs) with BoundedView {
initAttrs(context, attrs)
}
/*******************************************************************************/
/* */
/* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* */
/* This program is free software: you can redistribute it and/or modify */
/* it under the terms of the GNU General Public License as published by */
/* the Free Software Foundation, either version 3 of the License, or */
/* (at your option) any later version. */
/* */
/* This program is distributed in the hope that it will be useful, */
/* but WITHOUT ANY WARRANTY; without even the implied warranty of */
/* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the */
/* GNU General Public License for more details. */
/* */
/* You should have received a copy of the GNU General Public License */
/* along with this program. If not, see <http://www.gnu.org/licenses/>. */
/* */
/*******************************************************************************/
package com.github.shadowsocks.widget
import android.content.Context
import android.util.AttributeSet
import android.view.View
import android.view.View.MeasureSpec
import com.github.shadowsocks.R
/**
* Based on: http://stackoverflow.com/a/6212120/2245107
*
* @author Mygod
*/
trait BoundedView extends View {
private var boundedWidth: Int = _
private var boundedHeight: Int = _
protected def initAttrs(context: Context, attrs: AttributeSet) {
val arr = context.obtainStyledAttributes(attrs, R.styleable.BoundedView)
boundedWidth = arr.getDimensionPixelSize(R.styleable.BoundedView_bounded_width, 0)
boundedHeight = arr.getDimensionPixelSize(R.styleable.BoundedView_bounded_height, 0)
arr.recycle()
}
override def onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int): Unit = super.onMeasure(
if (boundedWidth > 0 && boundedWidth < MeasureSpec.getSize(widthMeasureSpec))
MeasureSpec.makeMeasureSpec(boundedWidth, MeasureSpec.getMode(widthMeasureSpec))
else widthMeasureSpec,
if (boundedHeight > 0 && boundedHeight < MeasureSpec.getSize(heightMeasureSpec))
MeasureSpec.makeMeasureSpec(boundedHeight, MeasureSpec.getMode(heightMeasureSpec))
else heightMeasureSpec)
}
/*******************************************************************************/
/* */
/* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* */
/* This program is free software: you can redistribute it and/or modify */
/* it under the terms of the GNU General Public License as published by */
/* the Free Software Foundation, either version 3 of the License, or */
/* (at your option) any later version. */
/* */
/* This program is distributed in the hope that it will be useful, */
/* but WITHOUT ANY WARRANTY; without even the implied warranty of */
/* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the */
/* GNU General Public License for more details. */
/* */
/* You should have received a copy of the GNU General Public License */
/* along with this program. If not, see <http://www.gnu.org/licenses/>. */
/* */
/*******************************************************************************/
package com.github.shadowsocks.widget
import android.support.design.widget.Snackbar
import android.view.View
import com.github.shadowsocks.R
import scala.collection.mutable.ArrayBuffer
/**
* @author Mygod
* @param view The view to find a parent from.
* @param undo Callback for undoing removals.
* @param commit Callback for committing removals.
* @tparam T Item type.
*/
class UndoSnackbarManager[T](view: View, undo: Iterator[(Int, T)] => Unit,
commit: Iterator[(Int, T)] => Unit = null) {
private val recycleBin = new ArrayBuffer[(Int, T)]
private val removedCallback = new Snackbar.Callback {
override def onDismissed(snackbar: Snackbar, event: Int) {
event match {
case Snackbar.Callback.DISMISS_EVENT_SWIPE | Snackbar.Callback.DISMISS_EVENT_MANUAL |
Snackbar.Callback.DISMISS_EVENT_TIMEOUT =>
if (commit != null) commit(recycleBin.iterator)
recycleBin.clear()
case _ =>
}
last = null
}
}
private var last: Snackbar = _
def remove(items: (Int, T)*) {
recycleBin.appendAll(items)
val count = recycleBin.length
last = Snackbar
.make(view, view.getResources.getQuantityString(R.plurals.removed, count, count: Integer), Snackbar.LENGTH_LONG)
.addCallback(removedCallback)
.setAction(R.string.undo, (_ => {
undo(recycleBin.reverseIterator)
recycleBin.clear
}): View.OnClickListener)
last.show()
}
def flush(): Unit = if (last != null) last.dismiss()
}
package scala.collection.mutable
import scala.collection.SortedSetLike
import scala.collection.generic.MutableSortedSetFactory
import scala.collection.immutable.RedBlackTree.Tree
import scala.collection.immutable.{RedBlackTree => RB}
import scala.runtime.ObjectRef
/**
* @define Coll `mutable.SortedList`
* @define coll mutable sorted list
* @factoryInfo
* Companion object of SortedList providing factory related utilities.
* @author Mygod
*/
//noinspection ReferenceMustBePrefixed,ScalaDocUnknownTag
object SortedList extends MutableSortedSetFactory[SortedList] {
/**
* The empty list of this type
*/
def empty[A](implicit ordering: Ordering[A]) = new SortedList[A]()
}
/**
* A mutable SortedList heavily based on SortedSet which "is not designed to enable meaningful subclassing".
*
* Based on: https://github.com/scala/scala/blob/3cc99d7/src/library/scala/collection/mutable/TreeSet.scala
*
* @author Mygod
*/
//noinspection ReferenceMustBePrefixed
class SortedList[A] private(treeRef: ObjectRef[RB.Tree[A, Null]], from: Option[A], until: Option[A])
(implicit val ordering: Ordering[A])
extends SortedSet[A] with SetLike[A, SortedList[A]] with SortedSetLike[A, SortedList[A]] with Set[A] with Serializable {
def insert(elem: A): Boolean = {
val old = size
this += elem
old < size
}
// AbstractSeq
def apply(i: Int): A = try RB.nth(treeRef.elem, i).key catch {
// out of bounds will result in accessing null nodes
case e: NullPointerException => throw new IndexOutOfBoundsException().initCause(e)
}
def length: Int = size
// Buffer
def +=:(elem: A): SortedList.this.type = +=(elem) // prepending is the same as the list is sorted
def remove(n: Int): A = {
val result = apply(n)
remove(result)
result
}
def update(idx: Int, elem: A) {
val x = apply(idx)
if (!ordering.equiv(x, elem)) {
remove(x)
this.add(elem)
}
}
// Optimized methods
override def clear(): Unit = treeRef.elem = null
def indexOf[B >: A](elem: B, from: Int = 0): Int = lookupIndex(treeRef.elem, elem.asInstanceOf[A]) match {
case result if result < from => -1
case result => result
}
override def last: A = RB.greatest(treeRef.elem).key
// Tree helper methods, TODO: expand to non-recursive form?
private def lookupIndex(tree: Tree[A, Null], x: A): Int =
if (tree eq null) -1 else ordering.compare(x, tree.key) match {
case 0 => RB.count(tree.left)
case cmp if cmp < 0 => lookupIndex(tree.left, x)
case _ => lookupIndex(tree.right, x) match {
case i if i < 0 => i
case i => RB.count(tree.left) + 1 + i
}
}
// The following is copied from TreeSet
if (ordering eq null)
throw new NullPointerException("ordering must not be null")
def this()(implicit ordering: Ordering[A]) = this(new ObjectRef(null), None, None)
override def size: Int = RB.countInRange(treeRef.elem, from, until)
override def stringPrefix = "SortedList"
override def empty: SortedList[A] = SortedList.empty
private def pickBound(comparison: (A, A) => A, oldBound: Option[A], newBound: Option[A]) = (newBound, oldBound) match {
case (Some(newB), Some(oldB)) => Some(comparison(newB, oldB))
case (None, _) => oldBound
case _ => newBound
}
override def rangeImpl(fromArg: Option[A], untilArg: Option[A]): SortedList[A] = {
val newFrom = pickBound(ordering.max, fromArg, from)
val newUntil = pickBound(ordering.min, untilArg, until)
new SortedList(treeRef, newFrom, newUntil)
}
override def -=(elem: A): this.type = {
treeRef.elem = RB.delete(treeRef.elem, elem)
this
}
override def +=(elem: A): this.type = {
treeRef.elem = RB.update(treeRef.elem, elem, null, overwrite = false)
this
}
/**
* Thanks to the immutable nature of the
* underlying Tree, we can share it with
* the clone. So clone complexity in time is O(1).
*
*/
override def clone(): SortedList[A] =
new SortedList[A](new ObjectRef(treeRef.elem), from, until)
private val notProjection = !(from.isDefined || until.isDefined)
override def contains(elem: A): Boolean = {
def leftAcceptable: Boolean = from match {
case Some(lb) => ordering.gteq(elem, lb)
case _ => true
}
def rightAcceptable: Boolean = until match {
case Some(ub) => ordering.lt(elem, ub)
case _ => true
}
(notProjection || (leftAcceptable && rightAcceptable)) &&
RB.contains(treeRef.elem, elem)
}
override def iterator: Iterator[A] = iteratorFrom(None)
override def keysIteratorFrom(start: A): Iterator[A] = iteratorFrom(Some(start))
private def iteratorFrom(start: Option[A]) = {
val it = RB.keysIterator(treeRef.elem, pickBound(ordering.max, from, start))
until match {
case None => it
case Some(ub) => it takeWhile (k => ordering.lt(k, ub))
}
}
}
package com.github.shadowsocks
import org.junit.Test
import org.junit.Assert.*
/**
* Example local unit test, which will execute on the development machine (host).
*
* @see [Testing documentation](http://d.android.com/tools/testing)
*/
class ExampleUnitTest {
@Test
fun addition_isCorrect() {
assertEquals(4, (2 + 2).toLong())
}
}
\ No newline at end of file
package com.github.shadowsocks.utils
import com.github.shadowsocks.utils.Subnet
import org.junit.Assert
import org.junit.Test
import java.net.InetAddress
object SubnetTest {
@Test
fun parsingAndEquals() {
Assert.assertEquals(Subnet(InetAddress.getByName("1.10.11.12"), 25), Subnet.fromString("1.10.11.12/25"))
Assert.assertEquals(Subnet(InetAddress.getByName("12.118.130.86"), 32), Subnet.fromString("12.118.130.86"))
Assert.assertEquals(Subnet(InetAddress.getByName("caec:cec6:c4ef:bb7b:1a78:d055:216d:3a78"), 96),
Subnet.fromString("caec:cec6:c4ef:bb7b:1a78:d055:216d:3a78/96"))
Assert.assertEquals(Subnet(InetAddress.getByName("be37:44bd:8630:7a0:2a3d:ff95:dd33:42f0"), 128),
Subnet.fromString("be37:44bd:8630:7a0:2a3d:ff95:dd33:42f0"))
Assert.assertNotEquals(Subnet.fromString("1.2.3.4/12"), Subnet.fromString("1.2.3.5/12"))
}
@Test(expected = IllegalArgumentException::class)
fun invalidParsing1() {
Subnet.fromString("caec:cec6:c4ef:bb7b:1a78:d055:216d:3a78/129")
}
@Test(expected = IllegalArgumentException::class)
fun invalidParsing2() {
Subnet.fromString("caec:cec6:c4ef:bb7b:1a78:d055:216d:3a78/-99")
}
@Test(expected = IllegalArgumentException::class)
fun invalidParsing3() {
Subnet.fromString("caec:cec6:c4ef:bb7b:1a78:d055:216d:3a78/1/0")
}
}
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
android {
compileSdkVersion rootProject.sdkVersion
defaultConfig {
minSdkVersion rootProject.minSdkVersion
targetSdkVersion rootProject.sdkVersion
versionCode 5
versionName "0.1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
}
}
}
dependencies {
api "com.android.support:preference-v7:$supportLibraryVersion"
api "com.android.support:preference-v14:$supportLibraryVersion"
api "com.takisoft.fix:preference-v7:$takisoftFixVersion"
api "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlinVersion"
testImplementation "junit:junit:$junitVersion"
androidTestImplementation "com.android.support.test:runner:$androidTestVersion"
androidTestImplementation "com.android.support.test.espresso:espresso-core:$androidEspressoVersion"
}
repositories {
mavenCentral()
}
enablePlugins(AndroidLib)
android.useSupportVectors
name := "plugin"
version := "0.0.4"
pomExtra in Global := {
<url>https://github.com/shadowsocks/shadowsocks-android</url>
<licenses>
<license>
<name>GPLv3</name>
<url>https://www.gnu.org/licenses/gpl-3.0.html</url>
</license>
</licenses>
<scm>
<connection>scm:git:git://github.com/shadowsocks/shadowsocks-android.git</connection>
<developerConnection>scm:git:git@github.com:shadowsocks/shadowsocks-android.git</developerConnection>
<url>github.com/shadowsocks/shadowsocks-android</url>
</scm>
<developers>
<developer>
<id>madeye</id>
<name>Max Lv</name>
<url>https://github.com/madeye</url>
</developer>
<developer>
<id>Mygod</id>
<name>Mygod Studio</name>
<url>https://github.com/Mygod</url>
</developer>
</developers>
}
...@@ -196,7 +196,7 @@ This feature hasn't been implemented yet. ...@@ -196,7 +196,7 @@ This feature hasn't been implemented yet.
Plugins are certified using package signatures and shadowsocks-android will consider these Plugins are certified using package signatures and shadowsocks-android will consider these
signatures as trusted: signatures as trusted:
* Signatures by [trusted sources](/mobile/src/main/scala/com/github/shadowsocks/plugin/PluginManager.scala#L22) * Signatures by [trusted sources](/mobile/src/main/java/com/github/shadowsocks/plugin/PluginManager.kt#L39)
which includes: which includes:
- @madeye, i.e. the signer of the main repo; - @madeye, i.e. the signer of the main repo;
- The main repo doesn't contain any other trusted signatures. Third-party forks should add their - The main repo doesn't contain any other trusted signatures. Third-party forks should add their
......
package com.github.shadowsocks.plugin;
import android.content.Context;
import android.support.test.InstrumentationRegistry;
import android.support.test.runner.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
import static org.junit.Assert.*;
/**
* Instrumented test, which will execute on an Android device.
*
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
@RunWith(AndroidJUnit4.class)
public class ExampleInstrumentedTest {
@Test
public void useAppContext() throws Exception {
// Context of the app under test.
Context appContext = InstrumentationRegistry.getTargetContext();
assertEquals("com.github.shadowsocks.plugin.test", appContext.getPackageName());
}
}
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.github.shadowsocks.plugin"> package="com.github.shadowsocks.plugin">
<uses-sdk
android:minSdkVersion="19"
android:targetSdkVersion="27"/>
<application <application
android:theme="@style/Theme.Shadowsocks"> android:theme="@style/Theme.Shadowsocks">
<meta-data android:name="com.github.shadowsocks.plugin.version" <meta-data android:name="com.github.shadowsocks.plugin.version"
android:value="0.0.4"/> android:value="0.1.0"/>
</application> </application>
</manifest> </manifest>
/*******************************************************************************
* *
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
* *
*******************************************************************************/
package com.github.shadowsocks.plugin
import android.app.Activity
import android.content.Intent
/**
* Base class for configuration activity. A configuration activity is started when user wishes to configure the
* selected plugin. To create a configuration activity, extend this class, implement abstract methods, invoke
* `saveChanges(options)` and `discardChanges()` when appropriate, and add it to your manifest like this:
*
* <pre class="prettyprint">&lt;manifest&gt;
* ...
* &lt;application&gt;
* ...
* &lt;activity android:name=".ConfigureActivity"&gt;
* &lt;intent-filter&gt;
* &lt;action android:name="com.github.shadowsocks.plugin.ACTION_CONFIGURE"/&gt;
* &lt;category android:name="android.intent.category.DEFAULT"/&gt;
* &lt;data android:scheme="plugin"
* android:host="com.github.shadowsocks"
* android:path="/$PLUGIN_ID"/&gt;
* &lt;/intent-filter&gt;
* &lt;/activity&gt;
* ...
* &lt;/application&gt;
*&lt;/manifest&gt;</pre>
*/
abstract class ConfigurationActivity : OptionsCapableActivity() {
/**
* Equivalent to setResult(RESULT_CANCELED).
*/
fun discardChanges() = setResult(Activity.RESULT_CANCELED)
/**
* Equivalent to setResult(RESULT_OK, args_with_correct_format).
*
* @param options PluginOptions to save.
*/
fun saveChanges(options: PluginOptions) =
setResult(Activity.RESULT_OK, Intent().putExtra(PluginContract.EXTRA_OPTIONS, options.toString()))
/**
* Finish this activity and request manual editor to pop up instead.
*/
fun fallbackToManualEditor() {
setResult(PluginContract.RESULT_FALLBACK)
finish()
}
}
/*******************************************************************************
* *
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
* *
*******************************************************************************/
package com.github.shadowsocks.plugin
/**
* Base class for a help activity. A help activity is started when user taps help when configuring options for your
* plugin. To create a help activity, just extend this class, and add it to your manifest like this:
*
* <pre class="prettyprint">&lt;manifest&gt;
* ...
* &lt;application&gt;
* ...
* &lt;activity android:name=".HelpActivity"&gt;
* &lt;intent-filter&gt;
* &lt;action android:name="com.github.shadowsocks.plugin.ACTION_HELP"/&gt;
* &lt;category android:name="android.intent.category.DEFAULT"/&gt;
* &lt;data android:scheme="plugin"
* android:host="com.github.shadowsocks"
* android:path="/$PLUGIN_ID"/&gt;
* &lt;/intent-filter&gt;
* &lt;/activity&gt;
* ...
* &lt;/application&gt;
*&lt;/manifest&gt;</pre>
*/
abstract class HelpActivity : OptionsCapableActivity() {
override fun onInitializePluginOptions(options: PluginOptions) { }
}
/*******************************************************************************
* *
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
* *
*******************************************************************************/
package com.github.shadowsocks.plugin
import android.content.Intent
/**
* HelpCallback is an HelpActivity but you just need to produce a CharSequence help message instead of having to
* provide UI. To create a help callback, just extend this class, implement abstract methods, and add it to your
* manifest following the same procedure as adding a HelpActivity.
*/
abstract class HelpCallback : HelpActivity() {
abstract fun produceHelpMessage(options: PluginOptions): CharSequence
override fun onInitializePluginOptions(options: PluginOptions) {
setResult(RESULT_OK, Intent().putExtra(PluginContract.EXTRA_HELP_MESSAGE, produceHelpMessage(options)))
finish()
}
}
/*******************************************************************************
* *
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
* *
*******************************************************************************/
package com.github.shadowsocks.plugin
import android.content.ContentProvider
import android.content.ContentValues
import android.database.Cursor
import android.database.MatrixCursor
import android.net.Uri
import android.os.Bundle
import android.os.ParcelFileDescriptor
/**
* Base class for a native plugin provider. A native plugin provider offers read-only access to files that are required
* to run a plugin, such as binary files and other configuration files. To create a native plugin provider, extend this
* class, implement the abstract methods, and add it to your manifest like this:
*
* <pre class="prettyprint">&lt;manifest&gt;
* ...
* &lt;application&gt;
* ...
* &lt;provider android:name="com.github.shadowsocks.$PLUGIN_ID.BinaryProvider"
* android:authorities="com.github.shadowsocks.plugin.$PLUGIN_ID.BinaryProvider"&gt;
* &lt;intent-filter&gt;
* &lt;category android:name="com.github.shadowsocks.plugin.ACTION_NATIVE_PLUGIN" /&gt;
* &lt;/intent-filter&gt;
* &lt;/provider&gt;
* ...
* &lt;/application&gt;
*&lt;/manifest&gt;</pre>
*/
abstract class NativePluginProvider : ContentProvider() {
override fun getType(p0: Uri?): String = "application/x-elf"
override fun onCreate(): Boolean = true
/**
* Provide all files needed for native plugin.
*
* @param provider A helper object to use to add files.
*/
protected abstract fun populateFiles(provider: PathProvider)
override fun query(uri: Uri, projection: Array<out String>?, selection: String?, selectionArgs: Array<out String>?,
sortOrder: String?): Cursor {
assert(selection == null && selectionArgs == null && sortOrder == null)
val result = MatrixCursor(projection)
populateFiles(PathProvider(uri, result))
return result
}
/**
* Returns executable entry absolute path. This is used if plugin is sharing UID with the host.
*
* Default behavior is throwing UnsupportedOperationException. If you don't wish to use this feature, use the default
* behavior.
*
* @return Absolute path for executable entry.
*/
open fun getExecutable(): String = throw UnsupportedOperationException()
abstract fun openFile(uri: Uri?): ParcelFileDescriptor
override fun openFile(uri: Uri?, mode: String?): ParcelFileDescriptor {
assert(mode == "r")
return openFile(uri)
}
override fun call(method: String?, arg: String?, extras: Bundle?): Bundle = when (method) {
PluginContract.METHOD_GET_EXECUTABLE -> {
val out = Bundle()
out.putString(PluginContract.EXTRA_ENTRY, getExecutable())
out
}
else -> super.call(method, arg, extras)
}
// Methods that should not be used
override fun insert(p0: Uri?, p1: ContentValues?): Uri = throw UnsupportedOperationException()
override fun update(p0: Uri?, p1: ContentValues?, p2: String?, p3: Array<out String>?): Int =
throw UnsupportedOperationException()
override fun delete(p0: Uri?, p1: String?, p2: Array<out String>?): Int = throw UnsupportedOperationException()
}
/*******************************************************************************
* *
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
* *
*******************************************************************************/
package com.github.shadowsocks.plugin
import android.content.Intent
import android.os.Bundle
import android.support.v7.app.AppCompatActivity
import android.widget.Toast
/**
* Activity that's capable of getting EXTRA_OPTIONS input.
*/
abstract class OptionsCapableActivity : AppCompatActivity() {
protected fun pluginOptions(intent: Intent = this.intent): PluginOptions {
return try {
PluginOptions(intent.getStringExtra(PluginContract.EXTRA_OPTIONS))
} catch (exc: IllegalArgumentException) {
Toast.makeText(this, exc.message, Toast.LENGTH_SHORT).show()
PluginOptions()
}
}
/**
* Populate args to your user interface.
*
* @param options PluginOptions parsed.
*/
protected abstract fun onInitializePluginOptions(options: PluginOptions = pluginOptions())
override fun onPostCreate(savedInstanceState: Bundle?) {
super.onPostCreate(savedInstanceState)
if (savedInstanceState == null) onInitializePluginOptions()
}
}
/*******************************************************************************
* *
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
* *
*******************************************************************************/
package com.github.shadowsocks.plugin
import android.database.MatrixCursor
import android.net.Uri
import java.io.File
/**
* Helper class to provide relative paths of files to copy.
*/
class PathProvider internal constructor(baseUri: Uri, private val cursor: MatrixCursor) {
private val basePath = baseUri.path?.trim('/') ?: ""
fun addPath(path: String, mode: String = "644"): PathProvider {
val trimmed = path.trim('/')
if (trimmed.startsWith(basePath)) cursor.newRow()
.add(PluginContract.COLUMN_PATH, trimmed)
.add(PluginContract.COLUMN_MODE, mode)
return this
}
fun addTo(file: File, to: String = "", mode: String = "644"): PathProvider {
var sub = to + file.name
if (basePath.startsWith(sub)) if (file.isDirectory) {
sub += '/'
file.listFiles().forEach { addTo(it, sub, mode) }
} else addPath(sub, mode)
return this
}
fun addAt(file: File, at: String = "", mode: String = "644"): PathProvider {
if (basePath.startsWith(at))
if (file.isDirectory) file.listFiles().forEach { addTo(it, at, mode) } else addPath(at, mode)
return this
}
}
/*******************************************************************************
* *
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
* *
*******************************************************************************/
package com.github.shadowsocks.plugin; package com.github.shadowsocks.plugin;
/** /**
* The contract between the plugin provider and host. Contains definitions for the supported actions, extras, etc. * The contract between the plugin provider and host. Contains definitions for the supported actions, extras, etc.
* *
* This class is written in Java to keep Java interoperability. * This class is written in Java to keep Java interoperability.
*
* @author Mygod
*/ */
public final class PluginContract { public final class PluginContract {
private PluginContract() { } private PluginContract() { }
......
/*******************************************************************************
* *
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
* *
*******************************************************************************/
package com.github.shadowsocks.plugin; package com.github.shadowsocks.plugin;
import android.text.TextUtils; import android.text.TextUtils;
...@@ -10,8 +30,6 @@ import java.util.StringTokenizer; ...@@ -10,8 +30,6 @@ import java.util.StringTokenizer;
* Helper class for processing plugin options. * Helper class for processing plugin options.
* *
* Based on: https://github.com/apache/ant/blob/588ce1f/src/main/org/apache/tools/ant/types/Commandline.java * Based on: https://github.com/apache/ant/blob/588ce1f/src/main/org/apache/tools/ant/types/Commandline.java
*
* @author Mygod
*/ */
public final class PluginOptions extends HashMap<String, String> { public final class PluginOptions extends HashMap<String, String> {
public PluginOptions() { public PluginOptions() {
......
package com.github.shadowsocks.plugin
import android.app.Activity
import android.content.Intent
/**
* Base class for configuration activity. A configuration activity is started when user wishes to configure the
* selected plugin. To create a configuration activity, extend this class, implement abstract methods, invoke
* `saveChanges(options)` and `discardChanges()` when appropriate, and add it to your manifest like this:
*
* <pre class="prettyprint">&lt;manifest&gt;
* ...
* &lt;application&gt;
* ...
* &lt;activity android:name=".ConfigureActivity"&gt;
* &lt;intent-filter&gt;
* &lt;action android:name="com.github.shadowsocks.plugin.ACTION_CONFIGURE"/&gt;
* &lt;category android:name="android.intent.category.DEFAULT"/&gt;
* &lt;data android:scheme="plugin"
* android:host="com.github.shadowsocks"
* android:path="/$PLUGIN_ID"/&gt;
* &lt;/intent-filter&gt;
* &lt;/activity&gt;
* ...
* &lt;/application&gt;
*&lt;/manifest&gt;</pre>
*
* @author Mygod
*/
trait ConfigurationActivity extends OptionsCapableActivity {
/**
* Equivalent to setResult(RESULT_CANCELED).
*/
final def discardChanges(): Unit = setResult(Activity.RESULT_CANCELED)
/**
* Equivalent to setResult(RESULT_OK, args_with_correct_format).
*
* @param options PluginOptions to save.
*/
final def saveChanges(options: PluginOptions): Unit =
setResult(Activity.RESULT_OK, new Intent().putExtra(PluginContract.EXTRA_OPTIONS, options.toString))
/**
* Finish this activity and request manual editor to pop up instead.
*/
final def fallbackToManualEditor() {
setResult(PluginContract.RESULT_FALLBACK)
finish()
}
}
package com.github.shadowsocks.plugin
/**
* Base class for a help activity. A help activity is started when user taps help when configuring options for your
* plugin. To create a help activity, just extend this class, and add it to your manifest like this:
*
* <pre class="prettyprint">&lt;manifest&gt;
* ...
* &lt;application&gt;
* ...
* &lt;activity android:name=".HelpActivity"&gt;
* &lt;intent-filter&gt;
* &lt;action android:name="com.github.shadowsocks.plugin.ACTION_HELP"/&gt;
* &lt;category android:name="android.intent.category.DEFAULT"/&gt;
* &lt;data android:scheme="plugin"
* android:host="com.github.shadowsocks"
* android:path="/$PLUGIN_ID"/&gt;
* &lt;/intent-filter&gt;
* &lt;/activity&gt;
* ...
* &lt;/application&gt;
*&lt;/manifest&gt;</pre>
*
* @author Mygod
*/
trait HelpActivity extends OptionsCapableActivity {
// HelpActivity can choose to ignore options
override protected def onInitializePluginOptions(options: PluginOptions): Unit = ()
}
package com.github.shadowsocks.plugin
import android.app.Activity
import android.content.Intent
/**
* HelpCallback is an HelpActivity but you just need to produce a CharSequence help message instead of having to
* provide UI. To create a help callback, just extend this class, implement abstract methods, and add it to your
* manifest following the same procedure as adding a HelpActivity.
*
* @author Mygod
*/
trait HelpCallback extends HelpActivity {
def produceHelpMessage(options: PluginOptions): CharSequence
override protected def onInitializePluginOptions(options: PluginOptions) {
setResult(Activity.RESULT_OK, new Intent().putExtra(PluginContract.EXTRA_HELP_MESSAGE, produceHelpMessage(options)))
finish()
}
}
package com.github.shadowsocks.plugin
import android.content.{ContentProvider, ContentValues}
import android.database.{Cursor, MatrixCursor}
import android.net.Uri
import android.os.{Bundle, ParcelFileDescriptor}
/**
* Base class for a native plugin provider. A native plugin provider offers read-only access to files that are required
* to run a plugin, such as binary files and other configuration files. To create a native plugin provider, extend this
* class, implement the abstract methods, and add it to your manifest like this:
*
* <pre class="prettyprint">&lt;manifest&gt;
* ...
* &lt;application&gt;
* ...
* &lt;provider android:name="com.github.shadowsocks.$PLUGIN_ID.BinaryProvider"
* android:authorities="com.github.shadowsocks.plugin.$PLUGIN_ID.BinaryProvider"&gt;
* &lt;intent-filter&gt;
* &lt;category android:name="com.github.shadowsocks.plugin.ACTION_NATIVE_PLUGIN" /&gt;
* &lt;/intent-filter&gt;
* &lt;/provider&gt;
* ...
* &lt;/application&gt;
*&lt;/manifest&gt;</pre>
*
* @author Mygod
*/
abstract class NativePluginProvider extends ContentProvider {
/**
* @inheritdoc
*
* NativePluginProvider returns application/x-elf by default. It's implementer's responsibility to change this to
* correct type.
*/
override def getType(uri: Uri): String = "application/x-elf"
override def onCreate(): Boolean = true
/**
* Provide all files needed for native plugin.
*
* @param provider A helper object to use to add files.
*/
protected def populateFiles(provider: PathProvider)
override def query(uri: Uri, projection: Array[String], selection: String, selectionArgs: Array[String],
sortOrder: String): Cursor = {
assert(selection == null && selectionArgs == null && sortOrder == null)
val result = new MatrixCursor(projection)
populateFiles(new PathProvider(uri, result))
result
}
/**
* Returns executable entry absolute path. This is used if plugin is sharing UID with the host.
*
* Default behavior is throwing UnsupportedOperationException. If you don't wish to use this feature, use the default
* behavior.
*
* @return Absolute path for executable entry.
*/
def getExecutable: String = throw new UnsupportedOperationException
def openFile(uri: Uri): ParcelFileDescriptor
override def openFile(uri: Uri, mode: String): ParcelFileDescriptor = {
assert(mode == "r")
openFile(uri)
}
override def call(method: String, arg: String, extras: Bundle): Bundle = method match {
case PluginContract.METHOD_GET_EXECUTABLE =>
val out = new Bundle()
out.putString(PluginContract.EXTRA_ENTRY, getExecutable)
out
case _ => super.call(method, arg, extras)
}
// Methods that should not be used
override def update(uri: Uri, values: ContentValues, selection: String, selectionArgs: Array[String]): Int = ???
override def insert(uri: Uri, values: ContentValues): Uri = ???
override def delete(uri: Uri, selection: String, selectionArgs: Array[String]): Int = ???
}
package com.github.shadowsocks.plugin
import android.content.Intent
import android.os.Bundle
import android.support.v7.app.AppCompatActivity
import android.widget.Toast
/**
* Activity that's capable of getting EXTRA_OPTIONS input.
*
* @author Mygod
*/
trait OptionsCapableActivity extends AppCompatActivity {
protected def pluginOptions(intent: Intent = getIntent): PluginOptions =
try new PluginOptions(intent.getStringExtra(PluginContract.EXTRA_OPTIONS)) catch {
case exc: IllegalArgumentException =>
Toast.makeText(this, exc.getMessage, Toast.LENGTH_SHORT).show()
null
}
/**
* Populate args to your user interface.
*
* @param options PluginOptions parsed.
*/
protected def onInitializePluginOptions(options: PluginOptions = pluginOptions()): Unit
override protected def onPostCreate(savedInstanceState: Bundle) {
super.onPostCreate(savedInstanceState)
if (savedInstanceState == null) onInitializePluginOptions()
}
}
package com.github.shadowsocks.plugin
import java.io.File
import android.database.MatrixCursor
import android.net.Uri
/**
* Helper class to provide relative paths of files to copy.
*
* @author Mygod
*/
final class PathProvider private[plugin](baseUri: Uri, cursor: MatrixCursor) {
private val basePath = baseUri.getPath match {
case null => ""
case p => p.stripPrefix("/").stripSuffix("/")
}
def addPath(path: String, mode: String = "644"): PathProvider = {
val stripped = path.stripPrefix("/").stripSuffix("/")
if (stripped.startsWith(basePath)) cursor.newRow()
.add(PluginContract.COLUMN_PATH, stripped)
.add(PluginContract.COLUMN_MODE, mode)
this
}
def addTo(file: File, to: String = "", mode: String = "644"): PathProvider = {
var sub = to + file.getName
if (basePath.startsWith(sub)) if (file.isDirectory) {
sub += '/'
file.listFiles().foreach(addTo(_, sub, mode))
} else addPath(sub, mode)
this
}
def addAt(file: File, at: String = "", mode: String = "644"): PathProvider = {
if (basePath.startsWith(at))
if (file.isDirectory) file.listFiles().foreach(addTo(_, at, mode)) else addPath(at, mode)
this
}
}
package com.github.shadowsocks.plugin;
import org.junit.Test;
import static org.junit.Assert.*;
/**
* Example local unit test, which will execute on the development machine (host).
*
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
public class ExampleUnitTest {
@Test
public void addition_isCorrect() throws Exception {
assertEquals(4, 2 + 2);
}
}
\ No newline at end of file
addSbtPlugin("org.scala-android" % "sbt-android" % "1.7.10")
addSbtPlugin("com.timushev.sbt" % "sbt-updates" % "0.1.10")
addSbtPlugin("net.virtual-void" % "sbt-dependency-graph" % "0.8.2")
addSbtPlugin("org.xerial.sbt" % "sbt-sonatype" % "1.1")
addSbtPlugin("com.jsuereth" % "sbt-pgp" % "1.0.0") // fot sbt-0.13.5 or higher
addSbtPlugin("org.scala-android" % "sbt-android-gms" % "0.4")
include ':mobile', ':core', ':plugin'
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment