Commit b9828671 authored by Max Lv's avatar Max Lv

Merge branch 'beta'

parents caf5731f 49171067
......@@ -29,7 +29,8 @@ The exclamation mark in the Wi-Fi/cellular icon appears because the system fails
1. Broken VPNService implementation, especially for IPv6;
2. Aggressive (or called broken) background service killing policy.
Fixes for MIUI: [#772](https://github.com/shadowsocks/shadowsocks-android/issues/772)
* Fixes for MIUI: [#772](https://github.com/shadowsocks/shadowsocks-android/issues/772)
* Fixes for Huawei: [#1091 (comment)](https://github.com/shadowsocks/shadowsocks-android/issues/1091#issuecomment-276949836)
### How to pause Shadowsocks service?
......
......@@ -30,7 +30,7 @@ _Put an `x` inside the [ ] that applies._
* [ ] Per-App Proxy
* [ ] Bypass mode
* [ ] UDP Forwarding
* KCP Parameters: (leave blank if not enabled)
* Plugin configuration (if applicable):
* [ ] Auto Connect
* [ ] TCP Fast Open
* [ ] NAT mode
......
classes
bin
gen
target
local.properties
.classpath
.project
.settings
tests/bin
tests/gen
tests/local.properties
NUL
gen_*
*.class
*.o
local.sbt
.deps
/proguard-sbt.txt
#Intellij IDEA
*.iml
*.ipr
*.iws
.idea/
out/
.d
#NDK
src/main/obj
src/main/libs
src/main/assets/armeabi-v7a/*
src/main/assets/x86/*
jni/Application.mk
mobile/src/main/libs
mobile/src/main/obj
[submodule "src/main/jni/shadowsocks-libev"]
path = src/main/jni/shadowsocks-libev
[submodule "mobile/src/main/jni/shadowsocks-libev"]
path = mobile/src/main/jni/shadowsocks-libev
url = https://github.com/shadowsocks/shadowsocks-libev
[submodule "src/main/jni/badvpn"]
path = src/main/jni/badvpn
[submodule "mobile/src/main/jni/badvpn"]
path = mobile/src/main/jni/badvpn
url = https://github.com/shadowsocks/badvpn.git
branch = shadowsocks-android
[submodule "src/main/jni/libancillary"]
path = src/main/jni/libancillary
[submodule "mobile/src/main/jni/libancillary"]
path = mobile/src/main/jni/libancillary
url = https://github.com/shadowsocks/libancillary.git
branch = shadowsocks-android
[submodule "src/main/jni/libevent"]
path = src/main/jni/libevent
[submodule "mobile/src/main/jni/libevent"]
path = mobile/src/main/jni/libevent
url = https://github.com/shadowsocks/libevent.git
branch = shadowsocks-android
[submodule "src/main/jni/redsocks"]
path = src/main/jni/redsocks
[submodule "mobile/src/main/jni/redsocks"]
path = mobile/src/main/jni/redsocks
url = https://github.com/shadowsocks/redsocks.git
branch = shadowsocks-android
[submodule "src/main/jni/pdnsd"]
path = src/main/jni/pdnsd
[submodule "mobile/src/main/jni/pdnsd"]
path = mobile/src/main/jni/pdnsd
url = https://github.com/shadowsocks/pdnsd.git
branch = shadowsocks-android
[submodule "kcptun/kcptun"]
path = kcptun/kcptun
url = https://github.com/shadowsocks/kcptun
[submodule "kcptun/go"]
path = kcptun/go
url = https://github.com/shadowsocks/go
[submodule "src/main/jni/mbedtls"]
path = src/main/jni/mbedtls
[submodule "mobile/src/main/jni/mbedtls"]
path = mobile/src/main/jni/mbedtls
url = https://github.com/ARMmbed/mbedtls
[submodule "src/main/jni/pcre"]
path = src/main/jni/pcre
[submodule "mobile/src/main/jni/pcre"]
path = mobile/src/main/jni/pcre
url = https://android.googlesource.com/platform/external/pcre
[submodule "mobile/src/main/jni/libsodium"]
path = mobile/src/main/jni/libsodium
url = https://github.com/jedisct1/libsodium.git
branch = stable
[submodule "mobile/src/main/jni/libev"]
path = mobile/src/main/jni/libev
url = https://github.com/shadowsocks/libev.git
[submodule "mobile/src/main/jni/libudns"]
path = mobile/src/main/jni/libudns
url = https://github.com/shadowsocks/libudns.git
......@@ -7,7 +7,6 @@ env:
global:
- NDK_VERSION=r12b
- NDK_CCACHE=ccache
- GOROOT_BOOTSTRAP=$GOROOT
- ANDROID_NDK_HOME=$HOME/.android/android-ndk-${NDK_VERSION}
- SBTPATH=$HOME/.sbt
- PATH=${ANDROID_NDK_HOME}:${ANDROID_HOME}/tools:${ANDROID_HOME}/platform-tools:${PATH}
......@@ -50,4 +49,4 @@ install:
fi
script:
- $SBTPATH/sbt -DNPROC=$(nproc) native-build android:package-debug
- $SBTPATH/sbt android:package
......@@ -12,7 +12,6 @@ A [shadowsocks](http://shadowsocks.org) client for Android, written in Scala.
* JDK 1.8
* SBT 0.13.0+
* Go 1.4+
* Android SDK
- Build Tools 25+
- Android Support Repository and Google Repository (see `build.sbt` for version)
......@@ -22,16 +21,15 @@ 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_NDK_HOME` to `/path/to/android-ndk`
* 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
* Create `local.properties` from `local.properties.example` with your own key information
* Create `mobile/local.properties` from `mobile/local.properties.example` with your own key information
* Invoke the building like this
```bash
git submodule update --init
git submodule update --init --recursive
# Build the App
sbt native-build clean android:package-release
sbt clean android:package-release
```
### TRANSLATE
......@@ -54,8 +52,8 @@ Translators can go to [POEditor](https://poeditor.com/join/project/u5VHO9vhSf) t
<li>shadowsocks-libev: <a href="https://github.com/shadowsocks/shadowsocks-libev/blob/master/LICENSE">GPLv3</a></li>
<li>pdnsd: <a href="https://github.com/shadowsocks/shadowsocks-android/blob/master/src/main/jni/pdnsd/COPYING">GPLv3</a></li>
<li>libev: <a href="https://github.com/shadowsocks/shadowsocks-libev/blob/master/libev/LICENSE">GPLv2</a></li>
<li>kcptun: <a href="https://github.com/xtaci/kcptun/commits/master/LICENSE.md">MIT</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>
### LICENSE
......
scalaVersion := "2.11.8"
dexMaxHeap := "4g"
enablePlugins(AndroidApp)
android.useSupportVectors
name := "shadowsocks"
version := "3.4.2"
versionCode := Some(174)
// Speed up ndk-build by reading nproc from environment
// and default to 8
val nproc = sys.props.getOrElse("NPROC", default = "8")
ndkArgs := Seq(s"-j$nproc")
platformTarget := "android-25"
compileOrder := CompileOrder.JavaThenScala
javacOptions ++= "-source" :: "1.7" :: "-target" :: "1.7" :: Nil
scalacOptions ++= "-target:jvm-1.7" :: "-Xexperimental" :: Nil
proguardVersion := "5.3.2"
proguardCache := Seq()
proguardOptions ++=
"-keep class com.github.shadowsocks.System { *; }" ::
"-dontwarn com.google.android.gms.internal.**" ::
"-dontwarn com.j256.ormlite.**" ::
"-dontwarn okio.**" ::
"-dontwarn org.xbill.**" ::
Nil
shrinkResources := true
typedResources := false
resConfigs := Seq("ja", "ru", "zh-rCN", "zh-rTW")
val supportLibsVersion = "25.1.0"
val playServicesVersion = "10.0.1"
resolvers += Resolver.jcenterRepo
libraryDependencies ++=
"com.android.support" % "cardview-v7" % supportLibsVersion ::
"com.android.support" % "customtabs" % supportLibsVersion ::
"com.android.support" % "design" % supportLibsVersion ::
"com.android.support" % "gridlayout-v7" % supportLibsVersion ::
"com.android.support" % "preference-v14" % supportLibsVersion ::
"com.futuremind.recyclerfastscroll" % "fastscroll" % "0.2.5" ::
"com.evernote" % "android-job" % "1.1.4" ::
"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.j256.ormlite" % "ormlite-android" % "5.0" ::
"com.mikepenz" % "crossfader" % "1.5.0" ::
"com.mikepenz" % "fastadapter" % "2.1.5" ::
"com.mikepenz" % "iconics-core" % "2.8.2" ::
"com.mikepenz" % "materialdrawer" % "5.8.1" ::
"com.mikepenz" % "materialize" % "1.0.0" ::
"com.squareup.okhttp3" % "okhttp" % "3.5.0" ::
"com.twofortyfouram" % "android-plugin-api-for-locale" % "1.0.2" ::
"dnsjava" % "dnsjava" % "2.1.7" ::
"eu.chainfire" % "libsuperuser" % "1.0.0.201608240809" ::
"net.glxn.qrgen" % "android" % "2.0" ::
Nil
lazy val nativeBuild = TaskKey[Unit]("native-build", "Build native executables")
nativeBuild := {
val logger = streams.value.log
Process("./build.sh") ! logger match {
case 0 => // Success!
case n => sys.error(s"Native build script exit code: $n")
}
}
lazy val commonSettings = Seq(
scalaVersion := "2.11.8",
dexMaxHeap := "4g",
organization := "com.github.shadowsocks",
platformTarget := "android-25",
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,
proguardVersion := "5.3.2",
proguardCache := Seq(),
shrinkResources := true,
typedResources := false
)
val supportLibsVersion = "25.1.1"
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
)
lazy val mobile = project
.settings(commonSettings)
.settings(
libraryDependencies ++=
"com.android.support" % "cardview-v7" % supportLibsVersion ::
"com.android.support" % "customtabs" % supportLibsVersion ::
"com.android.support" % "design" % supportLibsVersion ::
"com.android.support" % "gridlayout-v7" % supportLibsVersion ::
Nil
)
.dependsOn(plugin)
#!/bin/bash
function try () {
"$@" || exit -1
}
pushd src/main
# Clean up old binaries (no longer used)
rm -rf assets/armeabi-v7a assets/x86 libs/armeabi-v7a libs/x86
mkdir -p libs/armeabi-v7a libs/x86
popd
# Build kcptun
pushd kcptun
try ./make.bash
popd
Subproject commit c3f75e654ddb95bd53351294337649c08e9f9409
Subproject commit c532a50e1aeeaa9dee2428b374c3651a5c95258a
#!/bin/bash
function try () {
"$@" || exit -1
}
[ -z "$ANDROID_NDK_HOME" ] && ANDROID_NDK_HOME=~/android-ndk-r12b
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
DEPS=$DIR/.deps
ANDROID_ARM_TOOLCHAIN=$DEPS/android-toolchain-16-arm
ANDROID_X86_TOOLCHAIN=$DEPS/android-toolchain-16-x86
ANDROID_ARM_CC=$ANDROID_ARM_TOOLCHAIN/bin/arm-linux-androideabi-gcc
ANDROID_ARM_STRIP=$ANDROID_ARM_TOOLCHAIN/bin/arm-linux-androideabi-strip
ANDROID_X86_CC=$ANDROID_X86_TOOLCHAIN/bin/i686-linux-android-gcc
ANDROID_X86_STRIP=$ANDROID_X86_TOOLCHAIN/bin/i686-linux-android-strip
if [ ! -d "$DEPS" ]; then
mkdir -p $DEPS
fi
if [ ! -d "$ANDROID_ARM_TOOLCHAIN" ]; then
echo "Make standalone toolchain for ARM arch"
$ANDROID_NDK_HOME/build/tools/make_standalone_toolchain.py --arch arm \
--api 16 --install-dir $ANDROID_ARM_TOOLCHAIN
fi
if [ ! -d "$ANDROID_X86_TOOLCHAIN" ]; then
echo "Make standalone toolchain for X86 arch"
$ANDROID_NDK_HOME/build/tools/make_standalone_toolchain.py --arch x86 \
--api 16 --install-dir $ANDROID_X86_TOOLCHAIN
fi
if [ ! -d "$DIR/go/bin" ]; then
echo "Build the custom go"
pushd $DIR/go/src
try ./make.bash
popd
fi
export GOROOT=$DIR/go
export GOPATH=$DEPS/gopath
export GOBIN=$GOPATH/bin
mkdir -p $GOBIN
export PATH=$GOROOT/bin:$PATH
pushd kcptun/client
echo "Get dependences for kcptun"
go get -u github.com/xtaci/kcp-go
go get -u github.com/xtaci/smux
go get
echo "Cross compile kcptun for arm"
try env CGO_ENABLED=1 CC=$ANDROID_ARM_CC GOOS=android GOARCH=arm GOARM=7 go build -ldflags="-s -w"
try $ANDROID_ARM_STRIP client
try mv client $DIR/../src/main/libs/armeabi-v7a/libkcptun.so
echo "Cross compile kcptun for x86"
try env CGO_ENABLED=1 CC=$ANDROID_X86_CC GOOS=android GOARCH=386 go build -ldflags="-s -w"
try $ANDROID_X86_STRIP client
try mv client $DIR/../src/main/libs/x86/libkcptun.so
popd
echo "Successfully build kcptun"
enablePlugins(AndroidApp)
android.useSupportVectors
name := "shadowsocks"
version := "4.0.1"
versionCode := Some(176)
proguardOptions ++=
"-keep class com.github.shadowsocks.JniHelper { *; }" ::
"-dontwarn com.google.android.gms.internal.**" ::
"-dontwarn com.j256.ormlite.**" ::
"-dontwarn okio.**" ::
"-dontwarn org.xbill.**" ::
Nil
resConfigs := Seq("ja", "ru", "zh-rCN", "zh-rTW")
val playServicesVersion = "10.0.1"
resolvers += Resolver.jcenterRepo
libraryDependencies ++=
"com.futuremind.recyclerfastscroll" % "fastscroll" % "0.2.5" ::
"com.evernote" % "android-job" % "1.1.5" ::
"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.j256.ormlite" % "ormlite-android" % "5.0" ::
"com.mikepenz" % "crossfader" % "1.5.0" ::
"com.mikepenz" % "fastadapter" % "2.1.6" ::
"com.mikepenz" % "iconics-core" % "2.8.2" ::
"com.mikepenz" % "materialdrawer" % "5.8.1" ::
"com.mikepenz" % "materialize" % "1.0.0" ::
"com.squareup.okhttp3" % "okhttp" % "3.6.0" ::
"com.twofortyfouram" % "android-plugin-api-for-locale" % "1.0.2" ::
"dnsjava" % "dnsjava" % "2.1.8" ::
"eu.chainfire" % "libsuperuser" % "1.0.0.201608240809" ::
"net.glxn.qrgen" % "android" % "2.0" ::
Nil
......@@ -2,7 +2,6 @@
xmlns:tools="http://schemas.android.com/tools" package="com.github.shadowsocks">
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<uses-permission android:name="android.permission.NFC" />
......@@ -18,14 +17,10 @@
android:targetSdkVersion="25"/>
<application
android:allowBackup="true"
android:name=".ShadowsocksApplication"
android:hardwareAccelerated="true"
android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher"
android:backupAgent=".ShadowsocksBackupAgent"
android:label="@string/app_name"
android:theme="@style/Theme.Material"
android:banner="@drawable/ic_start_connected">
<meta-data android:name="com.google.android.gms.version"
......
......@@ -298,8 +298,8 @@ along with this program. If not, see <a href="http://www.gnu.org/licenses/">http
<li>shadowsocks-libev: <a href="https://github.com/shadowsocks/shadowsocks-libev/blob/master/LICENSE">GPLv3</a></li>
<li>pdnsd: <a href="https://github.com/shadowsocks/shadowsocks-android/blob/master/src/main/jni/pdnsd/COPYING">GPLv3</a></li>
<li>libev: <a href="https://github.com/shadowsocks/shadowsocks-libev/blob/master/libev/LICENSE">GPLv2</a></li>
<li>kcptun: <a href="https://github.com/xtaci/kcptun/commits/master/LICENSE.md">MIT</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>
</body>
......
......@@ -38,12 +38,23 @@
package com.github.shadowsocks;
public class System {
static {
java.lang.System.loadLibrary("system");
}
import android.os.Build;
import android.system.ErrnoException;
public static native int exec(String cmd);
public static native int sendfd(int fd, String path);
public static native void jniclose(int fd);
public class JniHelper {
static {
System.loadLibrary("jni-helper");
}
@Deprecated // Use Process.destroy() since API 24
public static void sigtermCompat(Process process) throws Exception {
if (Build.VERSION.SDK_INT >= 24) throw new UnsupportedOperationException("Never call this method in OpenJDK!");
int errno = sigterm(process);
if (errno != 0) throw Build.VERSION.SDK_INT >= 21
? new ErrnoException("kill", errno) : new Exception("kill failed: " + errno);
}
private static native int sigterm(Process process);
public static native int sendFd(int fd, String path);
public static native void close(int fd);
}
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()]);
}
}
......@@ -26,176 +26,37 @@ include $(CLEAR_VARS)
SODIUM_SOURCE := \
crypto_aead/chacha20poly1305/sodium/aead_chacha20poly1305.c \
crypto_auth/crypto_auth.c \
crypto_auth/hmacsha256/auth_hmacsha256_api.c \
crypto_auth/hmacsha256/cp/hmac_hmacsha256.c \
crypto_auth/hmacsha256/cp/verify_hmacsha256.c \
crypto_auth/hmacsha512/auth_hmacsha512_api.c \
crypto_auth/hmacsha512/cp/hmac_hmacsha512.c \
crypto_auth/hmacsha512/cp/verify_hmacsha512.c \
crypto_auth/hmacsha512256/auth_hmacsha512256_api.c \
crypto_auth/hmacsha512256/cp/hmac_hmacsha512256.c \
crypto_auth/hmacsha512256/cp/verify_hmacsha512256.c \
crypto_box/crypto_box.c \
crypto_box/crypto_box_easy.c \
crypto_box/crypto_box_seal.c \
crypto_box/curve25519xsalsa20poly1305/box_curve25519xsalsa20poly1305_api.c \
crypto_box/curve25519xsalsa20poly1305/ref/after_curve25519xsalsa20poly1305.c \
crypto_box/curve25519xsalsa20poly1305/ref/before_curve25519xsalsa20poly1305.c \
crypto_box/curve25519xsalsa20poly1305/ref/box_curve25519xsalsa20poly1305.c \
crypto_box/curve25519xsalsa20poly1305/ref/keypair_curve25519xsalsa20poly1305.c \
crypto_core/hsalsa20/ref2/core_hsalsa20.c \
crypto_core/hsalsa20/core_hsalsa20_api.c \
crypto_core/salsa20/ref/core_salsa20.c \
crypto_core/salsa20/core_salsa20_api.c \
crypto_generichash/crypto_generichash.c \
crypto_generichash/blake2/generichash_blake2_api.c \
crypto_generichash/blake2/ref/blake2-impl.h \
crypto_generichash/blake2/ref/blake2.h \
crypto_generichash/blake2/ref/blake2b-compress-ref.c \
crypto_generichash/blake2/ref/blake2b-load-sse2.h \
crypto_generichash/blake2/ref/blake2b-load-sse41.h \
crypto_generichash/blake2/ref/blake2b-ref.c \
crypto_generichash/blake2/ref/blake2b-round.h \
crypto_generichash/blake2/ref/generichash_blake2b.c \
crypto_hash/crypto_hash.c \
crypto_hash/sha256/hash_sha256_api.c \
crypto_hash/sha256/cp/hash_sha256.c \
crypto_hash/sha512/hash_sha512_api.c \
crypto_hash/sha512/cp/hash_sha512.c \
crypto_onetimeauth/crypto_onetimeauth.c \
crypto_onetimeauth/poly1305/onetimeauth_poly1305.c \
crypto_onetimeauth/poly1305/onetimeauth_poly1305.h \
crypto_onetimeauth/poly1305/donna/poly1305_donna.h \
crypto_onetimeauth/poly1305/donna/poly1305_donna32.h \
crypto_onetimeauth/poly1305/donna/poly1305_donna64.h \
crypto_onetimeauth/poly1305/donna/poly1305_donna.c \
crypto_pwhash/scryptsalsa208sha256/crypto_scrypt-common.c \
crypto_pwhash/scryptsalsa208sha256/crypto_scrypt.h \
crypto_pwhash/scryptsalsa208sha256/scrypt_platform.c \
crypto_pwhash/scryptsalsa208sha256/pbkdf2-sha256.c \
crypto_pwhash/scryptsalsa208sha256/pbkdf2-sha256.h \
crypto_pwhash/scryptsalsa208sha256/pwhash_scryptsalsa208sha256.c \
crypto_pwhash/scryptsalsa208sha256/sysendian.h \
crypto_pwhash/scryptsalsa208sha256/nosse/pwhash_scryptsalsa208sha256_nosse.c \
crypto_scalarmult/crypto_scalarmult.c \
crypto_pwhash/crypto_pwhash.c \
crypto_pwhash/argon2/argon2-core.c \
crypto_pwhash/argon2/argon2.c \
crypto_pwhash/argon2/argon2-encoding.c \
crypto_pwhash/argon2/argon2-fill-block-ref.c \
crypto_pwhash/argon2/blake2b-long.c \
crypto_pwhash/argon2/pwhash_argon2i.c \
crypto_scalarmult/curve25519/scalarmult_curve25519.c \
crypto_scalarmult/curve25519/scalarmult_curve25519.h \
crypto_secretbox/crypto_secretbox.c \
crypto_secretbox/crypto_secretbox_easy.c \
crypto_secretbox/xsalsa20poly1305/secretbox_xsalsa20poly1305_api.c \
crypto_secretbox/xsalsa20poly1305/ref/box_xsalsa20poly1305.c \
crypto_shorthash/crypto_shorthash.c \
crypto_shorthash/siphash24/shorthash_siphash24_api.c \
crypto_shorthash/siphash24/ref/shorthash_siphash24.c \
crypto_sign/crypto_sign.c \
crypto_sign/ed25519/ref10/base.h \
crypto_sign/ed25519/ref10/base2.h \
crypto_sign/ed25519/sign_ed25519_api.c \
crypto_sign/ed25519/ref10/d.h \
crypto_sign/ed25519/ref10/d2.h \
crypto_sign/ed25519/ref10/fe.h \
crypto_sign/ed25519/ref10/fe_0.c \
crypto_sign/ed25519/ref10/fe_1.c \
crypto_sign/ed25519/ref10/fe_add.c \
crypto_sign/ed25519/ref10/fe_cmov.c \
crypto_sign/ed25519/ref10/fe_copy.c \
crypto_sign/ed25519/ref10/fe_frombytes.c \
crypto_sign/ed25519/ref10/fe_invert.c \
crypto_sign/ed25519/ref10/fe_isnegative.c \
crypto_sign/ed25519/ref10/fe_isnonzero.c \
crypto_sign/ed25519/ref10/fe_mul.c \
crypto_sign/ed25519/ref10/fe_neg.c \
crypto_sign/ed25519/ref10/fe_pow22523.c \
crypto_sign/ed25519/ref10/fe_sq.c \
crypto_sign/ed25519/ref10/fe_sq2.c \
crypto_sign/ed25519/ref10/fe_sub.c \
crypto_sign/ed25519/ref10/fe_tobytes.c \
crypto_sign/ed25519/ref10/ge.h \
crypto_sign/ed25519/ref10/ge_add.c \
crypto_sign/ed25519/ref10/ge_add.h \
crypto_sign/ed25519/ref10/ge_double_scalarmult.c \
crypto_sign/ed25519/ref10/ge_frombytes.c \
crypto_sign/ed25519/ref10/ge_madd.c \
crypto_sign/ed25519/ref10/ge_madd.h \
crypto_sign/ed25519/ref10/ge_msub.c \
crypto_sign/ed25519/ref10/ge_msub.h \
crypto_sign/ed25519/ref10/ge_p1p1_to_p2.c \
crypto_sign/ed25519/ref10/ge_p1p1_to_p3.c \
crypto_sign/ed25519/ref10/ge_p2_0.c \
crypto_sign/ed25519/ref10/ge_p2_dbl.c \
crypto_sign/ed25519/ref10/ge_p2_dbl.h \
crypto_sign/ed25519/ref10/ge_p3_0.c \
crypto_sign/ed25519/ref10/ge_p3_dbl.c \
crypto_sign/ed25519/ref10/ge_p3_to_cached.c \
crypto_sign/ed25519/ref10/ge_p3_to_p2.c \
crypto_sign/ed25519/ref10/ge_p3_tobytes.c \
crypto_sign/ed25519/ref10/ge_precomp_0.c \
crypto_sign/ed25519/ref10/ge_scalarmult_base.c \
crypto_sign/ed25519/ref10/ge_sub.c \
crypto_sign/ed25519/ref10/ge_sub.h \
crypto_sign/ed25519/ref10/ge_tobytes.c \
crypto_sign/ed25519/ref10/keypair.c \
crypto_sign/ed25519/ref10/open.c \
crypto_sign/ed25519/ref10/pow22523.h \
crypto_sign/ed25519/ref10/pow225521.h \
crypto_sign/ed25519/ref10/sc.h \
crypto_sign/ed25519/ref10/sc_muladd.c \
crypto_sign/ed25519/ref10/sc_reduce.c \
crypto_sign/ed25519/ref10/sign.c \
crypto_sign/ed25519/ref10/sqrtm1.h \
crypto_stream/crypto_stream.c \
crypto_stream/chacha20/stream_chacha20.c \
crypto_stream/chacha20/stream_chacha20.h \
crypto_stream/chacha20/ref/stream_chacha20_ref.h \
crypto_stream/chacha20/ref/stream_chacha20_ref.c \
crypto_stream/salsa20/stream_salsa20_api.c \
crypto_stream/xsalsa20/stream_xsalsa20_api.c \
crypto_stream/xsalsa20/ref/stream_xsalsa20.c \
crypto_stream/xsalsa20/ref/xor_xsalsa20.c \
crypto_verify/16/verify_16_api.c \
crypto_stream/salsa20/ref/stream_salsa20_ref.c \
crypto_stream/salsa20/ref/xor_salsa20_ref.c \
crypto_verify/16/ref/verify_16.c \
crypto_verify/32/verify_32_api.c \
crypto_verify/32/ref/verify_32.c \
crypto_verify/64/verify_64_api.c \
crypto_verify/64/ref/verify_64.c \
randombytes/randombytes.c \
randombytes/sysrandom/randombytes_sysrandom.c \
sodium/core.c \
sodium/runtime.c \
sodium/utils.c \
sodium/version.c
SODIUM_SOURCE += \
crypto_scalarmult/curve25519/ref10/curve25519_ref10.c \
crypto_scalarmult/curve25519/ref10/curve25519_ref10.h \
crypto_scalarmult/curve25519/ref10/fe.h \
crypto_scalarmult/curve25519/ref10/fe_0_curve25519_ref10.c \
crypto_scalarmult/curve25519/ref10/fe_1_curve25519_ref10.c \
crypto_scalarmult/curve25519/ref10/fe_add_curve25519_ref10.c \
crypto_scalarmult/curve25519/ref10/fe_copy_curve25519_ref10.c \
crypto_scalarmult/curve25519/ref10/fe_cswap_curve25519_ref10.c \
crypto_scalarmult/curve25519/ref10/fe_frombytes_curve25519_ref10.c \
crypto_scalarmult/curve25519/ref10/fe_invert_curve25519_ref10.c \
crypto_scalarmult/curve25519/ref10/fe_mul_curve25519_ref10.c \
crypto_scalarmult/curve25519/ref10/fe_mul121666_curve25519_ref10.c \
crypto_scalarmult/curve25519/ref10/fe_sq_curve25519_ref10.c \
crypto_scalarmult/curve25519/ref10/fe_sub_curve25519_ref10.c \
crypto_scalarmult/curve25519/ref10/fe_tobytes_curve25519_ref10.c \
crypto_scalarmult/curve25519/ref10/montgomery.h \
crypto_scalarmult/curve25519/ref10/pow225521.h
SODIUM_SOURCE += \
crypto_stream/salsa20/ref/stream_salsa20_ref.c \
crypto_stream/salsa20/ref/xor_salsa20_ref.c
SODIUM_SOURCE += \
randombytes/sysrandom/randombytes_sysrandom.c
LOCAL_MODULE := sodium
LOCAL_CFLAGS += -O2 -I$(LOCAL_PATH)/shadowsocks-libev/libsodium/src/libsodium/include \
LOCAL_CFLAGS += -O2 -I$(LOCAL_PATH)/libsodium/src/libsodium/include \
-I$(LOCAL_PATH)/include \
-I$(LOCAL_PATH)/include/sodium \
-I$(LOCAL_PATH)/shadowsocks-libev/libsodium/src/libsodium/include/sodium \
-I$(LOCAL_PATH)/libsodium/src/libsodium/include/sodium \
-DPACKAGE_NAME=\"libsodium\" -DPACKAGE_TARNAME=\"libsodium\" \
-DPACKAGE_VERSION=\"1.0.7\" -DPACKAGE_STRING=\"libsodium\ 1.0.7\" \
-DPACKAGE_BUGREPORT=\"https://github.com/jedisct1/libsodium/issues\" \
......@@ -211,7 +72,7 @@ LOCAL_CFLAGS += -O2 -I$(LOCAL_PATH)/shadowsocks-libev/libsodium/src/libsodium/in
-DHAVE_WEAK_SYMBOLS=1 -DHAVE_ARC4RANDOM=1 -DHAVE_ARC4RANDOM_BUF=1 \
-DHAVE_MLOCK=1 -DHAVE_MPROTECT=1 -DHAVE_POSIX_MEMALIGN=1
LOCAL_SRC_FILES := $(addprefix shadowsocks-libev/libsodium/src/libsodium/,$(SODIUM_SOURCE))
LOCAL_SRC_FILES := $(addprefix libsodium/src/libsodium/,$(SODIUM_SOURCE))
include $(BUILD_STATIC_LIBRARY)
......@@ -275,7 +136,7 @@ LOCAL_MODULE := libipset
LOCAL_CFLAGS += -O2 -I$(LOCAL_PATH)/shadowsocks-libev/libipset/include \
-I$(LOCAL_PATH)/shadowsocks-libev/libcork/include
LOCAL_SRC_FILES := $(addprefix shadowsocks-libev/libipset/,$(IPSET_SOURCE))
LOCAL_SRC_FILES := $(addprefix shadowsocks-libev/libipset/src/libipset/,$(IPSET_SOURCE))
include $(BUILD_STATIC_LIBRARY)
......@@ -302,7 +163,7 @@ LOCAL_MODULE := libcork
LOCAL_CFLAGS += -O2 -I$(LOCAL_PATH)/shadowsocks-libev/libcork/include \
-DCORK_API=CORK_LOCAL
LOCAL_SRC_FILES := $(addprefix shadowsocks-libev/libcork/,$(CORK_SOURCE))
LOCAL_SRC_FILES := $(addprefix shadowsocks-libev/libcork/src/libcork/,$(CORK_SOURCE))
include $(BUILD_STATIC_LIBRARY)
......@@ -318,10 +179,10 @@ UDNS_SOURCES := udns_dn.c udns_dntosp.c udns_parse.c udns_resolver.c udns_init.c
udns_rr_srv.c udns_rr_naptr.c udns_codes.c udns_jran.c
LOCAL_MODULE := libudns
LOCAL_CFLAGS += -O2 -I$(LOCAL_PATH)/shadowsocks-libev/libudns \
LOCAL_CFLAGS += -O2 -I$(LOCAL_PATH)/libudns \
-DHAVE_DECL_INET_NTOP
LOCAL_SRC_FILES := $(addprefix shadowsocks-libev/libudns/,$(UDNS_SOURCES))
LOCAL_SRC_FILES := $(addprefix libudns/,$(UDNS_SOURCES))
include $(BUILD_STATIC_LIBRARY)
......@@ -335,8 +196,8 @@ LOCAL_MODULE := libev
LOCAL_CFLAGS += -O2 -DNDEBUG -DHAVE_CONFIG_H \
-I$(LOCAL_PATH)/include/libev
LOCAL_SRC_FILES := \
shadowsocks-libev/libev/ev.c \
shadowsocks-libev/libev/event.c
libev/ev.c \
libev/event.c
include $(BUILD_STATIC_LIBRARY)
......@@ -385,9 +246,11 @@ include $(BUILD_SHARED_EXECUTABLE)
include $(CLEAR_VARS)
SHADOWSOCKS_SOURCES := local.c cache.c udprelay.c encrypt.c \
utils.c netutils.c json.c jconf.c acl.c http.c tls.c rule.c \
obfs_http.c obfs_tls.c base64.c \
SHADOWSOCKS_SOURCES := local.c \
cache.c udprelay.c utils.c netutils.c json.c jconf.c \
acl.c http.c tls.c rule.c \
crypto.c aead.c stream.c base64.c \
plugin.c \
android.c
LOCAL_MODULE := ss-local
......@@ -400,12 +263,12 @@ LOCAL_CFLAGS := -Wall -O2 -fno-strict-aliasing -DMODULE_LOCAL \
-I$(LOCAL_PATH)/libancillary \
-I$(LOCAL_PATH)/mbedtls/include \
-I$(LOCAL_PATH)/pcre \
-I$(LOCAL_PATH)/shadowsocks-libev/libudns \
-I$(LOCAL_PATH)/libudns \
-I$(LOCAL_PATH)/shadowsocks-libev/libcork/include \
-I$(LOCAL_PATH)/shadowsocks-libev/libsodium/src/libsodium/include \
-I$(LOCAL_PATH)/shadowsocks-libev/libsodium/src/libsodium/include/sodium \
-I$(LOCAL_PATH)/libsodium/src/libsodium/include \
-I$(LOCAL_PATH)/libsodium/src/libsodium/include/sodium \
-I$(LOCAL_PATH)/shadowsocks-libev/libipset/include \
-I$(LOCAL_PATH)/shadowsocks-libev/libev
-I$(LOCAL_PATH)/libev
LOCAL_STATIC_LIBRARIES := libev libmbedtls libipset libcork libudns \
libsodium libancillary libpcre
......@@ -420,7 +283,11 @@ include $(BUILD_SHARED_EXECUTABLE)
include $(CLEAR_VARS)
SHADOWSOCKS_SOURCES := tunnel.c cache.c udprelay.c encrypt.c utils.c netutils.c json.c jconf.c android.c obfs_http.c obfs_tls.c base64.c
SHADOWSOCKS_SOURCES := tunnel.c \
cache.c udprelay.c utils.c netutils.c json.c jconf.c \
crypto.c aead.c stream.c base64.c \
plugin.c \
android.c
LOCAL_MODULE := ss-tunnel
LOCAL_SRC_FILES := $(addprefix shadowsocks-libev/src/, $(SHADOWSOCKS_SOURCES))
......@@ -429,12 +296,12 @@ LOCAL_CFLAGS := -Wall -O2 -fno-strict-aliasing -DMODULE_TUNNEL \
-DCONNECT_IN_PROGRESS=EINPROGRESS \
-I$(LOCAL_PATH)/libancillary \
-I$(LOCAL_PATH)/include \
-I$(LOCAL_PATH)/shadowsocks-libev/libudns \
-I$(LOCAL_PATH)/libudns \
-I$(LOCAL_PATH)/shadowsocks-libev/libcork/include \
-I$(LOCAL_PATH)/shadowsocks-libev/libsodium/src/libsodium/include \
-I$(LOCAL_PATH)/shadowsocks-libev/libsodium/src/libsodium/include/sodium \
-I$(LOCAL_PATH)/libsodium/src/libsodium/include \
-I$(LOCAL_PATH)/libsodium/src/libsodium/include/sodium \
-I$(LOCAL_PATH)/mbedtls/include \
-I$(LOCAL_PATH)/shadowsocks-libev/libev \
-I$(LOCAL_PATH)/libev \
-I$(LOCAL_PATH)/include/shadowsocks-libev
LOCAL_STATIC_LIBRARIES := libev libmbedtls libsodium libcork libudns libancillary
......@@ -444,16 +311,16 @@ LOCAL_LDLIBS := -llog
include $(BUILD_SHARED_EXECUTABLE)
########################################################
## system
## jni-helper
########################################################
include $(CLEAR_VARS)
LOCAL_MODULE:= system
LOCAL_MODULE:= jni-helper
LOCAL_C_INCLUDES:= $(LOCAL_PATH)/libancillary
LOCAL_SRC_FILES:= system.cpp
LOCAL_SRC_FILES:= jni-helper.cpp
LOCAL_LDLIBS := -ldl -llog
......
APP_ABI := armeabi-v7a x86
APP_PLATFORM := android-16
APP_PLATFORM := android-19
APP_STL := stlport_static
NDK_TOOLCHAIN_VERSION := clang
File moved
#define LOG_TAG "Shadowsocks"
#define LOG_TAG "JniHelper"
#include "jni.h"
#include <android/log.h>
#include <sys/system_properties.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <signal.h>
#include <sys/un.h>
#include <sys/stat.h>
......@@ -16,35 +19,33 @@
#define LOGI(...) do { __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__); } while(0)
#define LOGW(...) do { __android_log_print(ANDROID_LOG_WARN, LOG_TAG, __VA_ARGS__); } while(0)
#define LOGE(...) do { __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__); } while(0)
#define THROW(env, clazz, msg) do { env->ThrowNew(env->FindClass(clazz), msg); } while (0)
jint Java_com_github_shadowsocks_system_exec(JNIEnv *env, jobject thiz, jstring cmd) {
const char *cmd_str = env->GetStringUTFChars(cmd, 0);
static jclass ProcessImpl;
static jfieldID ProcessImpl_pid;
pid_t pid;
static int sdk_version() {
char version[PROP_VALUE_MAX + 1];
__system_property_get("ro.build.version.sdk", version);
return atoi(version);
}
/* Fork off the parent process */
pid = fork();
if (pid < 0) {
env->ReleaseStringUTFChars(cmd, cmd_str);
jint Java_com_github_shadowsocks_jnihelper_sigterm(JNIEnv *env, jobject thiz, jobject process) {
if (!env->IsInstanceOf(process, ProcessImpl)) {
THROW(env, "java/lang/ClassCastException",
"Unsupported process object. Only java.lang.ProcessManager$ProcessImpl is accepted.");
return -1;
}
if (pid > 0) {
env->ReleaseStringUTFChars(cmd, cmd_str);
return pid;
}
execl("/system/bin/sh", "sh", "-c", cmd_str, NULL);
env->ReleaseStringUTFChars(cmd, cmd_str);
return 1;
jint pid = env->GetIntField(process, ProcessImpl_pid);
// Suppress "No such process" errors. We just want the process killed. It's fine if it's already killed.
return kill(pid, SIGTERM) == -1 && errno != ESRCH ? errno : 0;
}
void Java_com_github_shadowsocks_system_jniclose(JNIEnv *env, jobject thiz, jint fd) {
void Java_com_github_shadowsocks_jnihelper_close(JNIEnv *env, jobject thiz, jint fd) {
close(fd);
}
jint Java_com_github_shadowsocks_system_sendfd(JNIEnv *env, jobject thiz, jint tun_fd, jstring path) {
jint Java_com_github_shadowsocks_jnihelper_sendfd(JNIEnv *env, jobject thiz, jint tun_fd, jstring path) {
int fd;
struct sockaddr_un addr;
const char *sock_str = env->GetStringUTFChars(path, 0);
......@@ -75,15 +76,15 @@ jint Java_com_github_shadowsocks_system_sendfd(JNIEnv *env, jobject thiz, jint t
return 0;
}
static const char *classPathName = "com/github/shadowsocks/System";
static const char *classPathName = "com/github/shadowsocks/JniHelper";
static JNINativeMethod method_table[] = {
{ "jniclose", "(I)V",
(void*) Java_com_github_shadowsocks_system_jniclose },
{ "sendfd", "(ILjava/lang/String;)I",
(void*) Java_com_github_shadowsocks_system_sendfd },
{ "exec", "(Ljava/lang/String;)I",
(void*) Java_com_github_shadowsocks_system_exec }
{ "close", "(I)V",
(void*) Java_com_github_shadowsocks_jnihelper_close },
{ "sendFd", "(ILjava/lang/String;)I",
(void*) Java_com_github_shadowsocks_jnihelper_sendfd },
{ "sigterm", "(Ljava/lang/Process;)I",
(void*) Java_com_github_shadowsocks_jnihelper_sigterm }
};
......@@ -139,20 +140,30 @@ jint JNI_OnLoad(JavaVM* vm, void* reserved) {
jint result = -1;
JNIEnv* env = NULL;
LOGI("JNI_OnLoad");
if (vm->GetEnv(&uenv.venv, JNI_VERSION_1_4) != JNI_OK) {
LOGE("ERROR: GetEnv failed");
if (vm->GetEnv(&uenv.venv, JNI_VERSION_1_6) != JNI_OK) {
THROW(env, "java/lang/RuntimeException", "GetEnv failed");
goto bail;
}
env = uenv.env;
if (registerNatives(env) != JNI_TRUE) {
LOGE("ERROR: registerNatives failed");
THROW(env, "java/lang/RuntimeException", "registerNativeMethods failed");
goto bail;
}
result = JNI_VERSION_1_4;
if (sdk_version() < 24) {
if (!(ProcessImpl = env->FindClass("java/lang/ProcessManager$ProcessImpl"))) {
THROW(env, "java/lang/RuntimeException", "ProcessManager$ProcessImpl not found");
goto bail;
}
ProcessImpl = (jclass) env->NewGlobalRef((jobject) ProcessImpl);
if (!(ProcessImpl_pid = env->GetFieldID(ProcessImpl, "pid", "I"))) {
THROW(env, "java/lang/RuntimeException", "ProcessManager$ProcessImpl.pid not found");
goto bail;
}
}
result = JNI_VERSION_1_6;
bail:
return result;
......
File moved
Subproject commit 981fb847e1e306bc40460de4a810f63f9bbc7212
File moved
Subproject commit e160de24fd146ab84b4eb397c7cffbf34775aa02
Subproject commit 53a1abd2b6fd7e3bf99ce6e2e267c636618d57ef
File moved
pcre @ 222bbf4b
File moved
File moved
File moved
Subproject commit f4b1641636942bc44c96160afad9f023fb6dbf0e
......@@ -10,6 +10,5 @@
<EditText android:id="@+id/content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textNoSuggestions"
android:lines="1"/>
android:inputType="textNoSuggestions|textMultiLine"/>
</LinearLayout>
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?android:attr/selectableItemBackground"
android:paddingStart="12dp"
android:paddingEnd="12dp">
<ImageView android:id="@android:id/icon"
android:layout_width="52dp"
android:layout_height="52dp"/>
<Space android:layout_width="4dp"
android:layout_height="wrap_content"/>
<LinearLayout android:orientation="vertical"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_gravity="center_vertical">
<TextView android:id="@android:id/text1"
android:textAppearance="?android:attr/textAppearance"
android:textColor="@android:color/primary_text_light"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:maxLines="2"
android:ellipsize="end"/>
<TextView android:id="@android:id/text2"
android:textAppearance="?android:attr/textAppearanceSmall"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:maxLines="2"
android:ellipsize="end"/>
</LinearLayout>
</LinearLayout>
......@@ -24,7 +24,7 @@
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:id="@+id/container">
<TextView android:id="@+id/title"
<TextView android:id="@android:id/text1"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
......@@ -54,27 +54,29 @@
android:contentDescription="@string/edit"
android:focusable="true"/>
</LinearLayout>
<LinearLayout android:layout_width="match_parent"
<RelativeLayout android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="4dp"
android:id="@+id/details">
<TextView android:id="@+id/address"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:maxLines="2"
android:ellipsize="end"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="?android:attr/textColorSecondary"/>
android:layout_marginTop="4dp">
<TextView android:id="@android:id/text2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_toStartOf="@+id/traffic"
android:layout_marginEnd="8dp"
android:maxLines="2"
android:ellipsize="end"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="?android:attr/textColorSecondary"/>
<TextView android:id="@+id/traffic"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:layout_alignParentEnd="true"
android:ellipsize="end"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="?android:attr/textColorSecondary"/>
</LinearLayout>
</RelativeLayout>
</LinearLayout>
</com.github.shadowsocks.widget.BoundedCardView>
</FrameLayout>
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string-array name="enc_method_entry">
<item>TABLE</item>
<item>RC4</item>
<item>RC4-MD5</item>
<item>AES-128-CFB</item>
<item>AES-192-CFB</item>
......@@ -17,11 +15,13 @@
<item>SALSA20</item>
<item>CHACHA20</item>
<item>CHACHA20-IETF</item>
<item>AES-128-GCM</item>
<item>AES-192-GCM</item>
<item>AES-256-GCM</item>
<item>CHACHA20-IETF-POLY1305</item>
</string-array>
<string-array name="enc_method_value">
<item>table</item>
<item>rc4</item>
<item>rc4-md5</item>
<item>aes-128-cfb</item>
<item>aes-192-cfb</item>
......@@ -36,6 +36,10 @@
<item>salsa20</item>
<item>chacha20</item>
<item>chacha20-ietf</item>
<item>aes-128-gcm</item>
<item>aes-192-gcm</item>
<item>aes-256-gcm</item>
<item>chacha20-ietf-poly1305</item>
</string-array>
<string-array name="route_entry">
......
......@@ -4,8 +4,4 @@
<attr name="bounded_width" format="dimension" />
<attr name="bounded_height" format="dimension" />
</declare-styleable>
<declare-styleable name="NumberPickerPreference">
<attr name="max" format="integer" />
<attr name="min" format="integer" />
</declare-styleable>
</resources>
<?xml version="1.0" encoding="utf-8"?>
<resources>
<dimen name="profile_item_max_width">480dp</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 -->
<dimen name="fab_margin_top">-44dp</dimen>
</resources>
......@@ -26,7 +26,6 @@
<string name="connection_test_error_status_code">Error code: #%d</string>
<!-- proxy category -->
<string name="proxy_cat">Server Settings</string>
<string name="profile_name">Profile Name</string>
<string name="proxy">Server</string>
<string name="remote_port">Remote Port</string>
......@@ -35,11 +34,8 @@
<string name="enc_method">Encrypt Method</string>
<!-- feature category -->
<string name="feature_cat">Feature Settings</string>
<string name="ipv6">IPv6 Route</string>
<string name="ipv6_summary">Redirect IPv6 traffic to remote</string>
<string name="onetime_auth">One-time Authentication</string>
<string name="onetime_auth_summary">Enable one-time authentication</string>
<string name="route_list">Route</string>
<string name="route_entry_all">All</string>
<string name="route_entry_bypass_lan">Bypass LAN</string>
......@@ -64,7 +60,6 @@
<string name="forward_success">Shadowsocks started.</string>
<string name="invalid_server">Invalid server name</string>
<string name="service_failed">Failed to connect the remote server</string>
<string name="method_unsafe">WARNING: The current encryption method is unsafe</string>
<string name="nat_deprecated">WARNING: NAT mode has been deprecated since Android 5.0</string>
<string name="nat_no_root">NAT mode requires ROOT permission</string>
<string name="switch_to_vpn">Switch to VPN mode</string>
......@@ -75,8 +70,6 @@
<string name="profile_invalid_input">No valid profile data found.</string>
<!-- alert category -->
<string name="yes">Yes</string>
<string name="no">No</string>
<string name="close">Close</string>
<string name="profile_empty">Please select a profile</string>
<string name="proxy_empty">Proxy/Password should not be empty</string>
......@@ -105,8 +98,6 @@
<!-- profile -->
<string name="profile_config">Profile config</string>
<string name="unsaved_changes_prompt">Changes not saved. Do you want to save?</string>
<string name="apply">Apply</string>
<string name="delete">Remove</string>
<string name="delete_confirm_prompt">Are you sure you want to remove this profile?</string>
<string name="share_qr_nfc">QR code/NFC</string>
......@@ -135,12 +126,6 @@
<string name="nat_connected">Connected</string>
<string name="not_connected">Not connected</string>
<!-- kcp -->
<string name="kcp_summary">Enable Shadowsocks over KCP</string>
<string name="kcp_port">KCP Port</string>
<string name="kcp_port_summary">%d (port number of the remote KCPTUN server)</string>
<string name="kcpcli">KCP Parameters (CLI)</string>
<!-- acl -->
<string name="custom_rules">Custom rules</string>
<string name="action_selection">Selection…</string>
......@@ -148,4 +133,12 @@
<string name="acl_rule_templates_generic">Subnet/Hostname PCRE pattern</string>
<string name="acl_rule_templates_domain">Domain name and all its subdomain names</string>
<string name="edit_rule">Edit rule</string>
<!-- plugin -->
<string name="plugin">Plugin</string>
<string name="plugin_configure">Configure…</string>
<string name="plugin_disabled">Disabled</string>
<string name="plugin_unknown">Unknown plugin %s</string>
<string name="plugin_untrusted">Warning: This plugin does not seem to come from a known trusted source.</string>
<string name="profile_plugin">Plugin: %s</string>
</resources>
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Theme.Shadowsocks.Material" parent="MaterialDrawerTheme.Light.DarkToolbar">
<item name="material_drawer_selected_text">@color/material_accent_200</item>
</style>
<style name="StyledScrollerTextAppearance" parent="@android:style/TextAppearance">
<item name="android:textSize">24sp</item>
<item name="android:textStyle">bold</item>
<item name="android:textColor">@android:color/white</item>
</style>
</resources>
......@@ -2,13 +2,14 @@
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res/com.github.shadowsocks">
<be.mygod.preference.EditTextPreference
android:key="profileName"
android:summary="%s"
android:title="@string/profile_name"/>
<be.mygod.preference.PreferenceCategory
android:title="@string/proxy_cat">
<be.mygod.preference.EditTextPreference
android:key="profileName"
android:summary="%s"
android:title="@string/profile_name"/>
<be.mygod.preference.EditTextPreference
android:key="proxy"
android:summary="%s"
......@@ -36,10 +37,6 @@
android:entryValues="@array/enc_method_value"
android:summary="%s"
android:title="@string/enc_method"/>
<SwitchPreference
android:key="isAuth"
android:summary="@string/onetime_auth_summary"
android:title="@string/onetime_auth"/>
</be.mygod.preference.PreferenceCategory>
......@@ -72,24 +69,18 @@
</be.mygod.preference.PreferenceCategory>
<be.mygod.preference.PreferenceCategory
android:title="KCP">
<SwitchPreference
android:key="kcp"
android:summary="@string/kcp_summary"
android:title="KCP"/>
<be.mygod.preference.NumberPickerPreference
app:min="1"
app:max="65535"
android:key="kcpPort"
android:summary="@string/kcp_port_summary"
android:title="@string/kcp_port"/>
android:title="@string/plugin">
<com.github.shadowsocks.preference.IconListPreference
android:key="plugin"
android:persistent="false"
android:summary="%s"
android:title="@string/plugin"/>
<be.mygod.preference.EditTextPreference
android:key="kcpcli"
android:key="plugin.configure"
android:persistent="false"
android:summary="%s"
android:title="@string/kcpcli"/>
android:title="@string/plugin_configure"/>
</be.mygod.preference.PreferenceCategory>
......
......@@ -48,21 +48,13 @@ object AppManager {
private var instance: AppManager = _
private var receiverRegistered: Boolean = _
private var receiver: BroadcastReceiver = _
private var cachedApps: Array[ProxiedApp] = _
private def getApps(pm: PackageManager) = {
if (!receiverRegistered) {
val filter = new IntentFilter(Intent.ACTION_PACKAGE_ADDED)
filter.addAction(Intent.ACTION_PACKAGE_REMOVED)
filter.addDataScheme("package")
app.registerReceiver((_: Context, intent: Intent) =>
if (intent.getAction != Intent.ACTION_PACKAGE_REMOVED ||
!intent.getBooleanExtra(Intent.EXTRA_REPLACING, false)) {
synchronized(cachedApps = null)
val instance = AppManager.instance
if (instance != null) instance.reloadApps()
}, filter)
receiverRegistered = true
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)
......
......@@ -20,7 +20,7 @@
package com.github.shadowsocks
import java.io.IOException
import java.io.{File, IOException}
import java.net.InetAddress
import java.util
import java.util.concurrent.TimeUnit
......@@ -28,7 +28,6 @@ import java.util.{Timer, TimerTask}
import android.app.Service
import android.content.{BroadcastReceiver, Context, Intent, IntentFilter}
import android.net.ConnectivityManager
import android.os.{Handler, IBinder, RemoteCallbackList}
import android.text.TextUtils
import android.util.Log
......@@ -37,19 +36,24 @@ import com.github.shadowsocks.ShadowsocksApplication.app
import com.github.shadowsocks.acl.Acl
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 okhttp3.{Dns, FormBody, OkHttpClient, Request}
import org.json.JSONObject
import scala.collection.mutable
import scala.collection.mutable.ArrayBuffer
import scala.collection.JavaConversions._
import scala.util.Random
trait BaseService extends Service {
@volatile private var state = State.STOPPED
@volatile protected var profile: Profile = _
@volatile private var plugin: PluginOptions = _
@volatile protected var pluginPath: String = _
case class NameNotResolvedException() extends IOException
case class KcpcliParseException(cause: Throwable) extends Exception(cause)
case class NullConnectionException() extends NullPointerException
var timer: Timer = _
......@@ -59,7 +63,6 @@ trait BaseService extends Service {
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)
lazy val protectPath: String = getApplicationInfo.dataDir + "/protect_path"
private val closeReceiver: BroadcastReceiver = (context: Context, _: Intent) => {
Toast.makeText(context, R.string.stopping, Toast.LENGTH_SHORT).show()
......@@ -67,19 +70,6 @@ trait BaseService extends Service {
}
var closeReceiverRegistered: Boolean = _
var kcptunProcess: GuardedProcess = _
private val networkReceiver: BroadcastReceiver = (context: Context, _: Intent) => {
val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE).asInstanceOf[ConnectivityManager]
val activeNetwork = cm.getActiveNetworkInfo
val isConnected = activeNetwork != null && activeNetwork.isConnected
if (isConnected && profile.kcp && kcptunProcess != null) {
restartHanlder.removeCallbacks(null)
restartHanlder.postDelayed(() => if (kcptunProcess != null) kcptunProcess.restart(), 2000)
}
}
var networkReceiverRegistered: Boolean = _
val binder = new IShadowsocksService.Stub {
override def getState: Int = {
state
......@@ -164,8 +154,12 @@ trait BaseService extends Service {
profile.password = proxy(2).trim
profile.method = proxy(3).trim
}
if (profile.route == Acl.CUSTOM_RULES) // rationalize custom rules
Acl.save(Acl.CUSTOM_RULES, new Acl().fromId(Acl.CUSTOM_RULES))
plugin = new PluginConfiguration(profile.plugin).selectedOptions
pluginPath = PluginManager.init(plugin)
}
def startRunner(profile: Profile) {
......@@ -185,25 +179,12 @@ trait BaseService extends Service {
closeReceiverRegistered = true
}
if (profile.kcp && !networkReceiverRegistered) {
// register network change receiver
val filter = new IntentFilter()
filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION)
registerReceiver(networkReceiver, filter)
networkReceiverRegistered = true
}
app.track(getClass.getSimpleName, "start")
changeState(State.CONNECTING)
if (profile.isMethodUnsafe)
handler.post(() => Toast.makeText(this, R.string.method_unsafe, Toast.LENGTH_LONG).show())
Utils.ThrowableFuture(try connect() catch {
case _: NameNotResolvedException => stopRunner(stopService = true, getString(R.string.invalid_server))
case exc: KcpcliParseException =>
stopRunner(stopService = true, getString(R.string.service_failed) + ": " + exc.cause.getMessage)
case _: NullConnectionException => stopRunner(stopService = true, getString(R.string.reboot_required))
case exc: Throwable =>
stopRunner(stopService = true, getString(R.string.service_failed) + ": " + exc.getMessage)
......@@ -218,10 +199,6 @@ trait BaseService extends Service {
unregisterReceiver(closeReceiver)
closeReceiverRegistered = false
}
if (networkReceiverRegistered) {
unregisterReceiver(networkReceiver)
networkReceiverRegistered = false
}
// Make sure update total traffic when stopping the runner
updateTrafficTotal(TrafficMonitor.txTotal, TrafficMonitor.rxTotal)
......@@ -333,4 +310,22 @@ trait BaseService extends Service {
case _: Exception => "exclude = " + default + ";"
}
}
protected def buildPluginCommandLine(): ArrayBuffer[String] = {
val result = ArrayBuffer(pluginPath)
if (TcpFastOpen.sendEnabled) result += "--fast-open"
result
}
protected final def buildShadowsocksConfig(file: String): String = {
val config = new JSONObject()
.put("server", profile.host)
.put("server_port", profile.remotePort)
.put("password", profile.password)
.put("method", profile.method)
if (pluginPath != null) config
.put("plugin", Commandline.toString(buildPluginCommandLine()))
.put("plugin_opts", plugin.toString)
IOUtils.writeString(new File(getFilesDir, file), config.toString)
file
}
}
......@@ -20,13 +20,16 @@
package com.github.shadowsocks
import android.content.SharedPreferences
import android.content.SharedPreferences.OnSharedPreferenceChangeListener
import android.os.Bundle
import android.support.design.widget.Snackbar
import android.support.v14.preference.SwitchPreference
import be.mygod.preference.PreferenceFragment
import com.github.shadowsocks.utils.{Key, TcpFastOpen}
import com.github.shadowsocks.ShadowsocksApplication.app
class GlobalConfigFragment extends PreferenceFragment {
class GlobalConfigFragment extends PreferenceFragment with OnSharedPreferenceChangeListener {
override def onCreatePreferences(bundle: Bundle, key: String) {
addPreferencesFromResource(R.xml.pref_global)
val switch = findPreference(Key.isAutoConnect).asInstanceOf[SwitchPreference]
......@@ -53,5 +56,16 @@ class GlobalConfigFragment extends PreferenceFragment {
tfo.setEnabled(false)
tfo.setSummary(getString(R.string.tcp_fastopen_summary_unsupported, java.lang.System.getProperty("os.version")))
}
app.settings.registerOnSharedPreferenceChangeListener(this)
}
override def onDestroy() {
app.settings.unregisterOnSharedPreferenceChangeListener(this)
super.onDestroy()
}
def onSharedPreferenceChanged(pref: SharedPreferences, key: String): Unit = key match {
case Key.isNAT => findPreference(key).asInstanceOf[SwitchPreference].setChecked(pref.getBoolean(key, false))
case _ =>
}
}
......@@ -24,8 +24,11 @@ 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
......@@ -40,7 +43,7 @@ class StreamLogger(is: InputStream, tag: String) extends Thread {
/**
* @author ayanamist@gmail.com
*/
class GuardedProcess(cmd: Seq[String]) {
class GuardedProcess(cmd: String*) {
private val TAG = classOf[GuardedProcess].getSimpleName
@volatile private var guardThread: Thread = _
......@@ -57,10 +60,13 @@ class GuardedProcess(cmd: Seq[String]) {
try {
var callback: () => Unit = null
while (!isDestroyed) {
Log.i(TAG, "start process: " + cmd)
if (BuildConfig.DEBUG) Log.d(TAG, "start process: " + Commandline.toString(cmd))
val startTime = currentTimeMillis
process = new ProcessBuilder(cmd).redirectErrorStream(true).start
process = new ProcessBuilder(cmd)
.redirectErrorStream(true)
.directory(app.getFilesDir)
.start()
val is = process.getInputStream
new StreamLogger(is, TAG).start()
......@@ -75,7 +81,7 @@ class GuardedProcess(cmd: Seq[String]) {
isRestart = false
} else {
if (currentTimeMillis - startTime < 1000) {
Log.w(TAG, "process exit too fast, stop guard: " + cmd)
Log.w(TAG, "process exit too fast, stop guard: " + Commandline.toString(cmd))
isDestroyed = true
}
}
......@@ -83,11 +89,11 @@ class GuardedProcess(cmd: Seq[String]) {
}
} catch {
case _: InterruptedException =>
Log.i(TAG, "thread interrupt, destroy process: " + cmd)
process.destroy()
if (BuildConfig.DEBUG) Log.d(TAG, "thread interrupt, destroy process: " + Commandline.toString(cmd))
destroyProcess()
case e: IOException => ioException = e
} finally semaphore.release()
}, "GuardThread-" + cmd)
}, "GuardThread-" + cmd.head)
guardThread.start()
semaphore.acquire()
......@@ -100,16 +106,24 @@ class GuardedProcess(cmd: Seq[String]) {
def destroy() {
isDestroyed = true
guardThread.interrupt()
process.destroy()
destroyProcess()
try guardThread.join() catch {
case _: InterruptedException =>
}
}
private def destroyProcess() {
if (Build.VERSION.SDK_INT < 24) {
JniHelper.sigtermCompat(process)
process.waitFor()
}
process.destroy()
}
def restart() {
this.synchronized {
isRestart = true
process.destroy()
destroyProcess()
}
}
......
......@@ -43,7 +43,7 @@ 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.CustomRulesFragment
import com.github.shadowsocks.acl.{Acl, CustomRulesFragment}
import com.github.shadowsocks.aidl.IShadowsocksServiceCallback
import com.github.shadowsocks.utils.CloseUtils.autoDisconnect
import com.github.shadowsocks.utils._
......@@ -84,11 +84,6 @@ class MainActivity extends Activity with ServiceBoundContext with Drawer.OnDrawe
private var txRateText: TextView = _
private var rxRateText: TextView = _
private var currentFragment: ToolbarFragment = _
private lazy val profilesFragment = new ProfilesFragment()
private lazy val customRulesFragment = new CustomRulesFragment()
private lazy val globalSettingsFragment = new GlobalSettingsFragment()
private lazy val aboutFragment = new AboutFragment()
private lazy val customTabsIntent = new CustomTabsIntent.Builder()
.setToolbarColor(ContextCompat.getColor(this, R.color.material_primary_500))
.build()
......@@ -170,13 +165,13 @@ class MainActivity extends Activity with ServiceBoundContext with Drawer.OnDrawe
override def onServiceDisconnected(): Unit = changeState(State.IDLE)
private def addDisableNatToSnackbar(snackbar: Snackbar) = snackbar.setAction(R.string.switch_to_vpn, (_ =>
if (state == State.STOPPED) app.editor.putBoolean(Key.isNAT, false)): View.OnClickListener)
if (state == State.STOPPED) app.editor.putBoolean(Key.isNAT, false).apply()): View.OnClickListener)
override def binderDied() {
override def binderDied(): Unit = handler.post(() => {
detachService()
app.crashRecovery()
attachService(callback)
}
})
override def onActivityResult(requestCode: Int, resultCode: Int, data: Intent): Unit = resultCode match {
case Activity.RESULT_OK => bgService.use(app.profileId)
......@@ -255,7 +250,7 @@ class MainActivity extends Activity with ServiceBoundContext with Drawer.OnDrawe
val tf = Typefaces.get(this, "fonts/Iceland.ttf")
if (tf != null) title.setTypeface(tf)
if (savedInstanceState == null) displayFragment(profilesFragment)
if (savedInstanceState == null) displayFragment(new ProfilesFragment)
statusText = findViewById(R.id.status).asInstanceOf[TextView]
txText = findViewById(R.id.tx).asInstanceOf[TextView]
txRateText = findViewById(R.id.txRate).asInstanceOf[TextView]
......@@ -267,7 +262,10 @@ class MainActivity extends Activity with ServiceBoundContext with Drawer.OnDrawe
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(new URL("https", "www.google.com", "/generate_204").openConnection()
autoDisconnect(new URL("https", app.currentProfile.get.route match {
case Acl.CHINALIST => "www.qualcomm.cn"
case _ => "www.google.com"
}, "/generate_204").openConnection()
.asInstanceOf[HttpURLConnection]) { conn =>
conn.setConnectTimeout(5 * 1000)
conn.setReadTimeout(5 * 1000)
......@@ -309,7 +307,7 @@ class MainActivity extends Activity with ServiceBoundContext with Drawer.OnDrawe
})
fab.setOnLongClickListener(_ => {
Utils.positionToast(Toast.makeText(this, if (state == State.CONNECTED) R.string.stop else R.string.connect,
Toast.LENGTH_SHORT), fab, getWindow, 0, Utils.dpToPx(this, 8)).show()
Toast.LENGTH_SHORT), fab, getWindow, 0, getResources.getDimensionPixelOffset(R.dimen.margin_small)).show()
true
})
......@@ -361,14 +359,13 @@ class MainActivity extends Activity with ServiceBoundContext with Drawer.OnDrawe
}
private def displayFragment(fragment: ToolbarFragment) {
currentFragment = fragment
getFragmentManager.beginTransaction().replace(R.id.fragment_holder, fragment).commitAllowingStateLoss()
drawer.closeDrawer()
}
override def onItemClick(view: View, position: Int, drawerItem: IDrawerItem[_, _ <: ViewHolder]): Boolean = {
drawerItem.getIdentifier match {
case DRAWER_PROFILES => displayFragment(profilesFragment)
case DRAWER_PROFILES => displayFragment(new ProfilesFragment)
case DRAWER_RECOVERY =>
app.track("GlobalConfigFragment", "reset")
if (bgService != null) bgService.use(-1)
......@@ -381,12 +378,12 @@ class MainActivity extends Activity with ServiceBoundContext with Drawer.OnDrawe
app.copyAssets()
handler.sendEmptyMessage(0)
}
case DRAWER_GLOBAL_SETTINGS => displayFragment(globalSettingsFragment)
case DRAWER_GLOBAL_SETTINGS => displayFragment(new GlobalSettingsFragment)
case DRAWER_ABOUT =>
app.track(TAG, "about")
displayFragment(aboutFragment)
displayFragment(new AboutFragment)
case DRAWER_FAQ => launchUrl(getString(R.string.faq_url))
case DRAWER_CUSTOM_RULES => displayFragment(customRulesFragment)
case DRAWER_CUSTOM_RULES => displayFragment(new CustomRulesFragment)
}
true // unexpected cases will throw exception
}
......@@ -405,11 +402,14 @@ class MainActivity extends Activity with ServiceBoundContext with Drawer.OnDrawe
setListeningForBandwidth(true)
}
override def onBackPressed(): Unit =
if (drawer.isDrawerOpen) drawer.closeDrawer() else if (currentFragment != profilesFragment) {
displayFragment(profilesFragment)
drawer.setSelection(DRAWER_PROFILES)
} else super.onBackPressed()
override def onBackPressed(): Unit = if (drawer.isDrawerOpen) drawer.closeDrawer() else {
val currentFragment = getFragmentManager.findFragmentById(R.id.fragment_holder).asInstanceOf[ToolbarFragment]
if (!currentFragment.onBackPressed())
if (currentFragment.isInstanceOf[ProfilesFragment]) super.onBackPressed() else {
displayFragment(new ProfilesFragment)
drawer.setSelection(DRAWER_PROFILES)
}
}
override def onStop() {
setListeningForBandwidth(false)
......
......@@ -21,14 +21,21 @@
package com.github.shadowsocks
import android.app.Activity
import android.content.DialogInterface
import android.content.{DialogInterface, Intent}
import android.os.Bundle
import android.support.v7.app.AlertDialog
import android.support.v7.widget.Toolbar
import com.github.shadowsocks.ShadowsocksApplication.app
import com.github.shadowsocks.plugin.PluginContract
import com.github.shadowsocks.utils.Key
object ProfileConfigActivity {
final val REQUEST_CODE_PLUGIN_HELP = 1
}
class ProfileConfigActivity extends Activity {
import ProfileConfigActivity._
private lazy val child = getFragmentManager.findFragmentById(R.id.content).asInstanceOf[ProfileConfigFragment]
override def onCreate(savedInstanceState: Bundle) {
......@@ -49,4 +56,12 @@ class ProfileConfigActivity extends Activity {
.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)
}
}
......@@ -20,24 +20,37 @@
package com.github.shadowsocks
import android.app.Activity
import android.content.SharedPreferences.OnSharedPreferenceChangeListener
import android.content.{DialogInterface, Intent, SharedPreferences}
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
import android.support.v7.widget.Toolbar.OnMenuItemClickListener
import android.text.TextUtils
import android.view.MenuItem
import be.mygod.preference.PreferenceFragment
import be.mygod.preference.{EditTextPreference, PreferenceFragment}
import com.github.shadowsocks.ShadowsocksApplication.app
import com.github.shadowsocks.database.Profile
import com.github.shadowsocks.preferences.KcpCliPreferenceDialogFragment
import com.github.shadowsocks.plugin._
import com.github.shadowsocks.preference.{IconListPreference, PluginConfigurationDialogFragment}
import com.github.shadowsocks.utils.{Action, Key, Utils}
object ProfileConfigFragment {
private final val REQUEST_CODE_PLUGIN_CONFIGURE = 1
}
class ProfileConfigFragment extends PreferenceFragment with OnMenuItemClickListener
with OnSharedPreferenceChangeListener {
import ProfileConfigFragment._
private var profile: Profile = _
private var isProxyApps: SwitchPreference = _
private var plugin: IconListPreference = _
private var pluginConfigure: EditTextPreference = _
private var pluginConfiguration: PluginConfiguration = _
override def onCreatePreferences(bundle: Bundle, key: String) {
app.profileManager.getProfile(getActivity.getIntent.getIntExtra(Action.EXTRA_PROFILE_ID, -1)) match {
......@@ -59,9 +72,51 @@ class ProfileConfigFragment extends PreferenceFragment with OnMenuItemClickListe
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.editor.putString(Key.plugin, pluginConfiguration.toString).putBoolean(Key.dirty, true).apply()
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.settings.registerOnSharedPreferenceChangeListener(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.settings.getString(Key.plugin, null))
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.editor.putString(Key.plugin, pluginConfiguration.toString).putBoolean(Key.dirty, true).apply()
true
} catch {
case exc: IllegalArgumentException =>
Snackbar.make(getView, exc.getLocalizedMessage, Snackbar.LENGTH_LONG).show()
false
}
override def onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String): Unit =
if (key != Key.proxyApps && findPreference(key) != null) app.editor.putBoolean(Key.dirty, true).apply()
......@@ -75,9 +130,31 @@ class ProfileConfigFragment extends PreferenceFragment with OnMenuItemClickListe
isProxyApps.setChecked(app.settings.getBoolean(Key.proxyApps, false)) // fetch proxyApps updated by AppManager
}
override def onDisplayPreferenceDialog(preference: Preference): Unit = preference.getKey match {
case Key.kcpcli => displayPreferenceDialog(Key.kcpcli, new KcpCliPreferenceDialogFragment())
case _ => super.onDisplayPreferenceDialog(preference)
private def showPluginEditor() {
val bundle = new Bundle()
bundle.putString(PluginConfigurationDialogFragment.PLUGIN_ID_FRAGMENT_TAG, pluginConfiguration.selected)
displayPreferenceDialog(Key.pluginConfigure, new PluginConfigurationDialogFragment, bundle)
}
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 {
......
......@@ -37,11 +37,12 @@ import android.view._
import android.widget.{LinearLayout, PopupMenu, TextView, Toast}
import com.github.shadowsocks.ShadowsocksApplication.app
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, NativeExpressAdView}
import scala.collection.mutable.ArrayBuffer
import scala.collection.mutable.{ArrayBuffer, ListBuffer}
import scala.util.Random
object ProfilesFragment {
......@@ -55,7 +56,7 @@ final class ProfilesFragment extends ToolbarFragment with Toolbar.OnMenuItemClic
private val cardButtonLongClickListener: OnLongClickListener = view => {
Utils.positionToast(Toast.makeText(getActivity, view.getContentDescription, Toast.LENGTH_SHORT), view,
getActivity.getWindow, 0, Utils.dpToPx(getActivity, 8)).show()
getActivity.getWindow, 0, getResources.getDimensionPixelOffset(R.dimen.margin_small)).show()
true
}
......@@ -77,8 +78,8 @@ final class ProfilesFragment extends ToolbarFragment with Toolbar.OnMenuItemClic
var item: Profile = _
private val title = itemView.findViewById(R.id.title).asInstanceOf[TextView]
private val address = itemView.findViewById(R.id.address).asInstanceOf[TextView]
private val text1 = itemView.findViewById(android.R.id.text1).asInstanceOf[TextView]
private val text2 = itemView.findViewById(android.R.id.text2).asInstanceOf[TextView]
private val traffic = itemView.findViewById(R.id.traffic).asInstanceOf[TextView]
private val edit = itemView.findViewById(R.id.edit)
edit.setOnClickListener(_ => startConfig(item.id))
......@@ -111,10 +112,22 @@ final class ProfilesFragment extends ToolbarFragment with Toolbar.OnMenuItemClic
tx += txTotal
rx += rxTotal
}
title.setText(item.getName)
address.setText(if (item.nameIsEmpty) "" else item.formattedAddress)
traffic.setText(if (tx <= 0 && rx <= 0) null else getString(R.string.stat_profiles,
TrafficMonitor.formatTraffic(tx), TrafficMonitor.formatTraffic(rx)))
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.profileId) {
itemView.setSelected(true)
......@@ -129,7 +142,7 @@ final class ProfilesFragment extends ToolbarFragment with Toolbar.OnMenuItemClic
val params =
new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)
params.gravity = Gravity.CENTER_HORIZONTAL
params.setMargins(0, Utils.dpToPx(getActivity, 8), 0, 0)
params.setMargins(0, getResources.getDimensionPixelOffset(R.dimen.margin_small), 0, 0)
adView = new NativeExpressAdView(getActivity)
adView.setLayoutParams(params)
adView.setAdUnitId("ca-app-pub-9097031975646651/5224027521")
......@@ -345,7 +358,9 @@ final class ProfilesFragment extends ToolbarFragment with Toolbar.OnMenuItemClic
REQUEST_SCAN_QR_CODE) catch {
case _: ActivityNotFoundException =>
Toast.makeText(getActivity, R.string.add_profile_scanner_not_installed, Toast.LENGTH_LONG).show()
startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("market://details?id=tw.com.quickmark")))
try startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("market://details?id=tw.com.quickmark"))) catch {
case exc: ActivityNotFoundException => exc.printStackTrace()
}
}
true
case R.id.action_import =>
......
......@@ -27,7 +27,6 @@ import android.nfc.{NdefMessage, NdefRecord, NfcAdapter}
import android.os.Bundle
import android.view.{LayoutInflater, View, ViewGroup}
import android.widget.{ImageView, LinearLayout}
import com.github.shadowsocks.utils.Utils
import net.glxn.qrgen.android.QRCode
object QRCodeDialog {
......@@ -50,8 +49,9 @@ final class QRCodeDialog extends DialogFragment {
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(Utils.dpToPx(getActivity, 250), Utils.dpToPx(getActivity, 250))
.withSize(size, size)
.asInstanceOf[QRCode].bitmap()
image.setImageBitmap(qrcode)
image
......
......@@ -20,14 +20,14 @@
package com.github.shadowsocks
import java.io.{FileOutputStream, IOException}
import java.io.{File, FileOutputStream, IOException}
import java.util
import java.util.Locale
import java.util.concurrent.TimeUnit
import android.annotation.SuppressLint
import android.app.Application
import android.content.SharedPreferences
import android.content._
import android.content.res.Configuration
import android.os.{Build, LocaleList}
import android.preference.PreferenceManager
......@@ -184,7 +184,7 @@ class ShadowsocksApplication extends Application {
for (task <- Executable.EXECUTABLES) {
cmd.append("killall lib%s.so".formatLocal(Locale.ENGLISH, task))
cmd.append("rm -f %1$s/%2$s-nat.conf %1$s/%2$s-vpn.conf"
.formatLocal(Locale.ENGLISH, getApplicationInfo.dataDir, task))
.formatLocal(Locale.ENGLISH, getFilesDir.getAbsolutePath, task))
}
if (app.isNatEnabled) {
cmd.append("iptables -t nat -F OUTPUT")
......@@ -204,10 +204,21 @@ class ShadowsocksApplication extends Application {
app.track(e)
}
if (files != null) for (file <- files) autoClose(assetManager.open("acl/" + file))(in =>
autoClose(new FileOutputStream(getApplicationInfo.dataDir + '/' + file))(out =>
autoClose(new FileOutputStream(new File(getFilesDir, file)))(out =>
IOUtils.copy(in, out)))
editor.putInt(Key.currentVersionCode, BuildConfig.VERSION_CODE).apply()
}
def updateAssets(): Unit = if (settings.getInt(Key.currentVersionCode, -1) != BuildConfig.VERSION_CODE) copyAssets()
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
}
}
......@@ -40,6 +40,6 @@ class ShadowsocksBackupAgent extends BackupAgentHelper {
val helper = new SharedPreferencesBackupHelper(this, PREFS_DISPLAY)
addHelper(MY_PREFS_BACKUP_KEY, helper)
addHelper(DATABASE, new FileBackupHelper(this, "../databases/" + DBHelper.PROFILE,
"../" + Acl.CUSTOM_RULES + ".acl"))
Acl.CUSTOM_RULES + ".acl"))
}
}
......@@ -53,148 +53,59 @@ class ShadowsocksNatService extends BaseService {
var su: Shell.Interactive = _
def startShadowsocksDaemon() {
val conf = if (profile.kcp) {
ConfigUtils
.SHADOWSOCKS.formatLocal(Locale.ENGLISH, "127.0.0.1", profile.localPort + 90, profile.localPort,
profile.password, profile.method, 600)
} else {
ConfigUtils
.SHADOWSOCKS.formatLocal(Locale.ENGLISH, profile.host, profile.remotePort, profile.localPort,
profile.password, profile.method, 600)
}
Utils.printToFile(new File(getApplicationInfo.dataDir + "/ss-local-nat.conf"))(p => {
p.println(conf)
})
val cmd = ArrayBuffer[String](getApplicationInfo.nativeLibraryDir + "/libss-local.so"
, "-b" , "127.0.0.1"
, "-t" , "600"
, "-P", getApplicationInfo.dataDir
, "-c" , getApplicationInfo.dataDir + "/ss-local-nat.conf")
if (profile.auth) cmd += "-A"
val cmd = ArrayBuffer[String](getApplicationInfo.nativeLibraryDir + "/libss-local.so",
"-b", "127.0.0.1",
"-l", profile.localPort.toString,
"-t", "600",
"-c", buildShadowsocksConfig("ss-local-nat.conf"))
if (TcpFastOpen.sendEnabled) cmd += "--fast-open"
if (profile.route != Acl.ALL) {
cmd += "--acl"
cmd += Acl.getPath(profile.route)
cmd += Acl.getFile(profile.route).getAbsolutePath
}
if (BuildConfig.DEBUG) Log.d(TAG, cmd.mkString(" "))
sslocalProcess = new GuardedProcess(cmd).start()
}
def startKcptunDaemon() {
if (profile.kcpcli == null) profile.kcpcli = ""
val host = if (profile.host.contains(":")) {
"[" + profile.host + "]"
} else {
profile.host
}
val cmd = ArrayBuffer[String](getApplicationInfo.nativeLibraryDir + "/libkcptun.so"
, "-r", host + ":" + profile.kcpPort
, "-l", "127.0.0.1:" + (profile.localPort + 90))
try cmd ++= Utils.translateCommandline(profile.kcpcli) catch {
case exc: Exception => throw KcpcliParseException(exc)
}
if (BuildConfig.DEBUG) Log.d(TAG, cmd.mkString(" "))
kcptunProcess = new GuardedProcess(cmd).start()
sslocalProcess = new GuardedProcess(cmd: _*).start()
}
def startDNSTunnel() {
if (profile.udpdns) {
val conf = ConfigUtils
.SHADOWSOCKS.formatLocal(Locale.ENGLISH, profile.host, profile.remotePort, profile.localPort + 53,
profile.password, profile.method, 10)
Utils.printToFile(new File(getApplicationInfo.dataDir + "/ss-tunnel-nat.conf"))(p => {
p.println(conf)
})
val cmd = ArrayBuffer[String](getApplicationInfo.nativeLibraryDir + "/libss-tunnel.so"
, "-u"
, "-t" , "10"
, "-b" , "127.0.0.1"
, "-l" , (profile.localPort + 53).toString
, "-L" , if (profile.remoteDns == null) "8.8.8.8:53" else profile.remoteDns + ":53"
, "-P" , getApplicationInfo.dataDir
, "-c" , getApplicationInfo.dataDir + "/ss-tunnel-nat.conf")
if (profile.auth) cmd += "-A"
if (BuildConfig.DEBUG) Log.d(TAG, cmd.mkString(" "))
val cmd = ArrayBuffer[String](getApplicationInfo.nativeLibraryDir + "/libss-tunnel.so",
"-t", "10",
"-b", "127.0.0.1",
"-l", (profile.localPort + 63).toString,
"-L", profile.remoteDns + ":53",
"-c", buildShadowsocksConfig("ss-tunnel-nat.conf"))
sstunnelProcess = new GuardedProcess(cmd).start()
if (profile.udpdns) cmd.append("-u")
} else {
val conf = if (profile.kcp) {
ConfigUtils
.SHADOWSOCKS.formatLocal(Locale.ENGLISH, "127.0.0.1", profile.localPort + 90,
profile.localPort + 63, profile.password, profile.method, 10)
} else {
ConfigUtils
.SHADOWSOCKS.formatLocal(Locale.ENGLISH, profile.host, profile.remotePort, profile.localPort + 63,
profile.password, profile.method, 10)
}
Utils.printToFile(new File(getApplicationInfo.dataDir + "/ss-tunnel-nat.conf"))(p => {
p.println(conf)
})
val cmdBuf = ArrayBuffer[String](getApplicationInfo.nativeLibraryDir + "/libss-tunnel.so"
, "-t" , "10"
, "-b" , "127.0.0.1"
, "-l" , (profile.localPort + 63).toString
, "-L" , if (profile.remoteDns == null) "8.8.8.8:53" else profile.remoteDns + ":53"
, "-P", getApplicationInfo.dataDir
, "-c" , getApplicationInfo.dataDir + "/ss-tunnel-nat.conf")
if (profile.auth) cmdBuf += "-A"
if (BuildConfig.DEBUG) Log.d(TAG, cmdBuf.mkString(" "))
sstunnelProcess = new GuardedProcess(cmdBuf).start()
}
sstunnelProcess = new GuardedProcess(cmd: _*).start()
}
def startDnsDaemon() {
val reject = if (profile.ipv6) "224.0.0.0/3" else "224.0.0.0/3, ::/0"
val conf = profile.route match {
IOUtils.writeString(new File(getFilesDir, "pdnsd-nat.conf"), profile.route match {
case Acl.BYPASS_CHN | Acl.BYPASS_LAN_CHN | Acl.GFWLIST | Acl.CUSTOM_RULES =>
ConfigUtils.PDNSD_DIRECT.formatLocal(Locale.ENGLISH, "", getApplicationInfo.dataDir,
ConfigUtils.PDNSD_DIRECT.formatLocal(Locale.ENGLISH, "", getCacheDir.getAbsolutePath,
"127.0.0.1", profile.localPort + 53, "114.114.114.114, 223.5.5.5, 1.2.4.8",
getBlackList, reject, profile.localPort + 63, reject)
case Acl.CHINALIST =>
ConfigUtils.PDNSD_DIRECT.formatLocal(Locale.ENGLISH, "", getApplicationInfo.dataDir,
ConfigUtils.PDNSD_DIRECT.formatLocal(Locale.ENGLISH, "", getCacheDir.getAbsolutePath,
"127.0.0.1", profile.localPort + 53, "8.8.8.8, 8.8.4.4, 208.67.222.222",
"", reject, profile.localPort + 63, reject)
case _ =>
ConfigUtils.PDNSD_LOCAL.formatLocal(Locale.ENGLISH, "", getApplicationInfo.dataDir,
ConfigUtils.PDNSD_LOCAL.formatLocal(Locale.ENGLISH, "", getCacheDir.getAbsolutePath,
"127.0.0.1", profile.localPort + 53, profile.localPort + 63, reject)
}
Utils.printToFile(new File(getApplicationInfo.dataDir + "/pdnsd-nat.conf"))(p => {
p.println(conf)
})
val cmd = Array(getApplicationInfo.nativeLibraryDir + "/libpdnsd.so", "-c", getApplicationInfo.dataDir + "/pdnsd-nat.conf")
if (BuildConfig.DEBUG) Log.d(TAG, cmd.mkString(" "))
pdnsdProcess = new GuardedProcess(cmd).start()
pdnsdProcess = new GuardedProcess(getApplicationInfo.nativeLibraryDir + "/libpdnsd.so", "-c", "pdnsd-nat.conf")
.start()
}
def startRedsocksDaemon() {
val conf = ConfigUtils.REDSOCKS.formatLocal(Locale.ENGLISH, profile.localPort)
val cmd = Array(getApplicationInfo.nativeLibraryDir + "/libredsocks.so", "-c", getApplicationInfo.dataDir + "/redsocks-nat.conf")
Utils.printToFile(new File(getApplicationInfo.dataDir + "/redsocks-nat.conf"))(p => {
p.println(conf)
})
if (BuildConfig.DEBUG) Log.d(TAG, cmd.mkString(" "))
redsocksProcess = new GuardedProcess(cmd).start()
IOUtils.writeString(new File(getFilesDir, "redsocks-nat.conf"),
ConfigUtils.REDSOCKS.formatLocal(Locale.ENGLISH, profile.localPort))
redsocksProcess = new GuardedProcess(getApplicationInfo.nativeLibraryDir + "/libredsocks.so",
"-c", "redsocks-nat.conf").start()
}
/** Called when the activity is first created. */
......@@ -205,7 +116,6 @@ class ShadowsocksNatService extends BaseService {
startShadowsocksDaemon()
if (!profile.udpdns) startDnsDaemon()
if (profile.kcp) startKcptunDaemon()
setupIptables()
......@@ -221,10 +131,6 @@ class ShadowsocksNatService extends BaseService {
}
def killProcesses() {
if (kcptunProcess != null) {
kcptunProcess.destroy()
kcptunProcess = null
}
if (sslocalProcess != null) {
sslocalProcess.destroy()
sslocalProcess = null
......
......@@ -23,14 +23,13 @@ package com.github.shadowsocks
import java.io.File
import java.util.Locale
import android.annotation.SuppressLint
import android.content._
import android.content.pm.PackageManager.NameNotFoundException
import android.net.VpnService
import android.os._
import android.util.Log
import com.github.shadowsocks.ShadowsocksApplication.app
import com.github.shadowsocks.acl.{AclSyncJob, Acl}
import com.github.shadowsocks.acl.{Acl, AclSyncJob, Subnet}
import com.github.shadowsocks.database.Profile
import com.github.shadowsocks.utils._
......@@ -91,10 +90,6 @@ class ShadowsocksVpnService extends VpnService with BaseService {
}
def killProcesses() {
if (kcptunProcess != null) {
kcptunProcess.destroy()
kcptunProcess = null
}
if (sslocalProcess != null) {
sslocalProcess.destroy()
sslocalProcess = null
......@@ -157,157 +152,65 @@ class ShadowsocksVpnService extends VpnService with BaseService {
val fd = startVpn()
if (!sendFd(fd)) throw new Exception("sendFd failed")
if (profile.kcp) {
startKcptunDaemon()
}
startShadowsocksDaemon()
if (profile.udpdns && profile.kcp) {
startShadowsocksUDPDaemon()
}
if (!profile.udpdns) {
startDnsDaemon()
startDnsTunnel()
}
}
def startKcptunDaemon() {
if (profile.kcpcli == null) profile.kcpcli = ""
val host = if (profile.host.contains(":")) {
"[" + profile.host + "]"
} else {
profile.host
}
val cmd = ArrayBuffer(getApplicationInfo.nativeLibraryDir + "/libkcptun.so"
, "-r", host + ":" + profile.kcpPort
, "-l", "127.0.0.1:" + (profile.localPort + 90)
, "--path", protectPath)
try cmd ++= Utils.translateCommandline(profile.kcpcli) catch {
case exc: Exception => throw KcpcliParseException(exc)
}
if (BuildConfig.DEBUG)
Log.d(TAG, cmd.mkString(" "))
kcptunProcess = new GuardedProcess(cmd).start()
}
def startShadowsocksUDPDaemon() {
val conf = ConfigUtils
.SHADOWSOCKS.formatLocal(Locale.ENGLISH, profile.host, profile.remotePort, profile.localPort,
profile.password, profile.method, 600)
Utils.printToFile(new File(getApplicationInfo.dataDir + "/ss-local-udp-vpn.conf"))(p => {
p.println(conf)
})
val cmd = ArrayBuffer[String](getApplicationInfo.nativeLibraryDir + "/libss-local.so", "-V", "-U"
, "-b", "127.0.0.1"
, "-t", "600"
, "-P", getApplicationInfo.dataDir
, "-c", getApplicationInfo.dataDir + "/ss-local-udp-vpn.conf")
if (profile.auth) cmd += "-A"
if (BuildConfig.DEBUG) Log.d(TAG, cmd.mkString(" "))
sstunnelProcess = new GuardedProcess(cmd).start()
}
override protected def buildPluginCommandLine(): ArrayBuffer[String] = super.buildPluginCommandLine() += "-V"
def startShadowsocksDaemon() {
val conf = if (profile.kcp) {
ConfigUtils
.SHADOWSOCKS.formatLocal(Locale.ENGLISH, "127.0.0.1", profile.localPort + 90, profile.localPort,
profile.password, profile.method, 600)
} else {
ConfigUtils
.SHADOWSOCKS.formatLocal(Locale.ENGLISH, profile.host, profile.remotePort, profile.localPort,
profile.password, profile.method, 600)
}
Utils.printToFile(new File(getApplicationInfo.dataDir + "/ss-local-vpn.conf"))(p => {
p.println(conf)
})
val cmd = ArrayBuffer[String](getApplicationInfo.nativeLibraryDir + "/libss-local.so", "-V"
, "-b", "127.0.0.1"
, "-t", "600"
, "-P", getApplicationInfo.dataDir
, "-c", getApplicationInfo.dataDir + "/ss-local-vpn.conf")
val cmd = ArrayBuffer[String](getApplicationInfo.nativeLibraryDir + "/libss-local.so",
"-V",
"-b", "127.0.0.1",
"-l", profile.localPort.toString,
"-t", "600",
"-c", buildShadowsocksConfig("ss-local-vpn.conf"))
if (profile.auth) cmd += "-A"
if (profile.udpdns && !profile.kcp) cmd += "-u"
if (profile.udpdns) cmd += "-u"
if (profile.route != Acl.ALL) {
cmd += "--acl"
cmd += Acl.getPath(profile.route)
cmd += Acl.getFile(profile.route).getAbsolutePath
}
if (TcpFastOpen.sendEnabled) cmd += "--fast-open"
if (BuildConfig.DEBUG) Log.d(TAG, cmd.mkString(" "))
sslocalProcess = new GuardedProcess(cmd).start()
sslocalProcess = new GuardedProcess(cmd: _*).start()
}
def startDnsTunnel() {
val conf = if (profile.kcp) {
ConfigUtils
.SHADOWSOCKS.formatLocal(Locale.ENGLISH, "127.0.0.1", profile.localPort + 90, profile.localPort + 63,
profile.password, profile.method, 10)
} else {
ConfigUtils
.SHADOWSOCKS.formatLocal(Locale.ENGLISH, profile.host, profile.remotePort, profile.localPort + 63,
profile.password, profile.method, 10)
}
Utils.printToFile(new File(getApplicationInfo.dataDir + "/ss-tunnel-vpn.conf"))(p => {
p.println(conf)
})
val cmd = ArrayBuffer[String](getApplicationInfo.nativeLibraryDir + "/libss-tunnel.so"
, "-V"
, "-t", "10"
, "-b", "127.0.0.1"
, "-L" , if (profile.remoteDns == null) "8.8.8.8:53" else profile.remoteDns + ":53"
, "-P", getApplicationInfo.dataDir
, "-c", getApplicationInfo.dataDir + "/ss-tunnel-vpn.conf")
if (profile.auth) cmd += "-A"
if (BuildConfig.DEBUG) Log.d(TAG, cmd.mkString(" "))
sstunnelProcess = new GuardedProcess(cmd).start()
}
def startDnsTunnel(): Unit =
sstunnelProcess = new GuardedProcess(getApplicationInfo.nativeLibraryDir + "/libss-tunnel.so",
"-V",
"-t", "10",
"-b", "127.0.0.1",
"-l", (profile.localPort + 63).toString,
"-L" , profile.remoteDns + ":53",
"-c", buildShadowsocksConfig("ss-tunnel-vpn.conf")).start()
def startDnsDaemon() {
val reject = if (profile.ipv6) "224.0.0.0/3" else "224.0.0.0/3, ::/0"
val protect = "protect = \"" + protectPath +"\";"
val conf = profile.route match {
IOUtils.writeString(new File(getFilesDir, "pdnsd-vpn.conf"), profile.route match {
case Acl.BYPASS_CHN | Acl.BYPASS_LAN_CHN | Acl.GFWLIST | Acl.CUSTOM_RULES =>
ConfigUtils.PDNSD_DIRECT.formatLocal(Locale.ENGLISH, protect, getApplicationInfo.dataDir,
ConfigUtils.PDNSD_DIRECT.formatLocal(Locale.ENGLISH, "protect = \"protect_path\";", getCacheDir.getAbsolutePath,
"0.0.0.0", profile.localPort + 53, "114.114.114.114, 223.5.5.5, 1.2.4.8",
getBlackList, reject, profile.localPort + 63, reject)
case Acl.CHINALIST =>
ConfigUtils.PDNSD_DIRECT.formatLocal(Locale.ENGLISH, protect, getApplicationInfo.dataDir,
ConfigUtils.PDNSD_DIRECT.formatLocal(Locale.ENGLISH, "protect = \"protect_path\";", getCacheDir.getAbsolutePath,
"0.0.0.0", profile.localPort + 53, "8.8.8.8, 8.8.4.4, 208.67.222.222",
"", reject, profile.localPort + 63, reject)
case _ =>
ConfigUtils.PDNSD_LOCAL.formatLocal(Locale.ENGLISH, protect, getApplicationInfo.dataDir,
ConfigUtils.PDNSD_LOCAL.formatLocal(Locale.ENGLISH, "protect = \"protect_path\";", getCacheDir.getAbsolutePath,
"0.0.0.0", profile.localPort + 53, profile.localPort + 63, reject)
}
Utils.printToFile(new File(getApplicationInfo.dataDir + "/pdnsd-vpn.conf"))(p => {
p.println(conf)
})
val cmd = Array(getApplicationInfo.nativeLibraryDir + "/libpdnsd.so", "-c", getApplicationInfo.dataDir + "/pdnsd-vpn.conf")
if (BuildConfig.DEBUG) Log.d(TAG, cmd.mkString(" "))
val cmd = Array(getApplicationInfo.nativeLibraryDir + "/libpdnsd.so", "-c", "pdnsd-vpn.conf")
pdnsdProcess = new GuardedProcess(cmd).start()
pdnsdProcess = new GuardedProcess(cmd: _*).start()
}
@SuppressLint(Array("NewApi"))
def startVpn(): Int = {
val builder = new Builder()
......@@ -316,7 +219,7 @@ class ShadowsocksVpnService extends VpnService with BaseService {
.setMtu(VPN_MTU)
.addAddress(PRIVATE_VLAN.formatLocal(Locale.ENGLISH, "1"), 24)
builder.addDnsServer("8.8.8.8")
builder.addDnsServer(profile.remoteDns)
if (profile.ipv6) {
builder.addAddress(PRIVATE_VLAN6.formatLocal(Locale.ENGLISH, "1"), 126)
......@@ -344,15 +247,13 @@ class ShadowsocksVpnService extends VpnService with BaseService {
if (profile.route == Acl.ALL || profile.route == Acl.BYPASS_CHN) {
builder.addRoute("0.0.0.0", 0)
} else {
val privateList = getResources.getStringArray(R.array.bypass_private_route)
privateList.foreach(cidr => {
val addr = cidr.split('/')
builder.addRoute(addr(0), addr(1).toInt)
getResources.getStringArray(R.array.bypass_private_route).foreach(cidr => {
val subnet = Subnet.fromString(cidr)
builder.addRoute(subnet.address.getHostAddress, subnet.prefixSize)
})
builder.addRoute(profile.remoteDns, 32)
}
builder.addRoute("8.8.0.0", 16)
conn = builder.establish()
if (conn == null) throw new NullConnectionException
......@@ -364,7 +265,7 @@ class ShadowsocksVpnService extends VpnService with BaseService {
"--socks-server-addr", "127.0.0.1:" + profile.localPort,
"--tunfd", fd.toString,
"--tunmtu", VPN_MTU.toString,
"--sock-path", getApplicationInfo.dataDir + "/sock_path",
"--sock-path", "sock_path",
"--loglevel", "3")
if (profile.ipv6)
......@@ -376,9 +277,7 @@ class ShadowsocksVpnService extends VpnService with BaseService {
cmd += ("--dnsgw", "%s:%d".formatLocal(Locale.ENGLISH, PRIVATE_VLAN.formatLocal(Locale.ENGLISH, "1"),
profile.localPort + 53))
if (BuildConfig.DEBUG) Log.d(TAG, cmd.mkString(" "))
tun2socksProcess = new GuardedProcess(cmd).start(() => sendFd(fd))
tun2socksProcess = new GuardedProcess(cmd: _*).start(() => sendFd(fd))
fd
}
......@@ -388,7 +287,7 @@ class ShadowsocksVpnService extends VpnService with BaseService {
var tries = 1
while (tries < 5) {
Thread.sleep(1000 * tries)
if (System.sendfd(fd, getApplicationInfo.dataDir + "/sock_path") != -1) {
if (JniHelper.sendFd(fd, new File(getFilesDir, "sock_path").getAbsolutePath) != -1) {
return true
}
tries += 1
......
......@@ -32,11 +32,11 @@ object ShadowsocksVpnThread {
val getInt: Method = classOf[FileDescriptor].getDeclaredMethod("getInt$")
}
class ShadowsocksVpnThread(vpnService: ShadowsocksVpnService) extends Thread {
class ShadowsocksVpnThread(service: ShadowsocksVpnService) extends Thread {
import ShadowsocksVpnThread._
val TAG = "ShadowsocksVpnService"
lazy val PATH: String = vpnService.getApplicationInfo.dataDir + "/protect_path"
val protect = new File(service.getFilesDir, "protect_path")
@volatile var isRunning: Boolean = true
@volatile var serverSocket: LocalServerSocket = _
......@@ -59,11 +59,11 @@ class ShadowsocksVpnThread(vpnService: ShadowsocksVpnService) extends Thread {
override def run() {
new File(PATH).delete()
protect.delete()
try {
val localSocket = new LocalSocket
localSocket.bind(new LocalSocketAddress(PATH, LocalSocketAddress.Namespace.FILESYSTEM))
localSocket.bind(new LocalSocketAddress(protect.getAbsolutePath, LocalSocketAddress.Namespace.FILESYSTEM))
serverSocket = new LocalServerSocket(localSocket.getFileDescriptor)
} catch {
case e: IOException =>
......@@ -89,10 +89,10 @@ class ShadowsocksVpnThread(vpnService: ShadowsocksVpnService) extends Thread {
if (fds.nonEmpty) {
val fd = getInt.invoke(fds(0)).asInstanceOf[Int]
val ret = vpnService.protect(fd)
val ret = service.protect(fd)
// Trick to close file decriptor
System.jniclose(fd)
JniHelper.close(fd)
if (ret) {
output.write(0)
......
......@@ -36,4 +36,6 @@ class ToolbarFragment extends Fragment {
}
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.FileNotFoundException
import java.io.{File, FileNotFoundException}
import com.github.shadowsocks.ShadowsocksApplication.app
import com.github.shadowsocks.utils.IOUtils
......@@ -83,7 +83,7 @@ class Acl {
this.subnets ++= (if (bypass) proxySubnets else bypassSubnets)
this
}
final def fromId(id: String): Acl = fromSource(Source.fromFile(Acl.getPath(id)))
final def fromId(id: String): Acl = fromSource(Source.fromFile(Acl.getFile(id)))
override def toString: String = {
val result = new StringBuilder()
......@@ -128,7 +128,7 @@ object Acl {
final val CHINALIST = "china-list"
final val CUSTOM_RULES = "custom-rules"
def getPath(id: String): String = app.getApplicationInfo.dataDir + '/' + id + ".acl"
def getFile(id: String) = new File(app.getFilesDir, id + ".acl")
def customRules: Acl = {
val acl = new Acl()
try acl.fromId(CUSTOM_RULES) catch {
......@@ -138,5 +138,5 @@ object Acl {
acl.bypassHostnames.clear() // everything is bypassed
acl
}
def save(id: String, acl: Acl): Unit = IOUtils.writeString(getPath(id), acl.toString)
def save(id: String, acl: Acl): Unit = IOUtils.writeString(getFile(id), acl.toString)
}
......@@ -25,7 +25,6 @@ import java.util.concurrent.TimeUnit
import com.evernote.android.job.Job.{Params, Result}
import com.evernote.android.job.{Job, JobRequest}
import com.github.shadowsocks.ShadowsocksApplication.app
import com.github.shadowsocks.utils.IOUtils
import scala.io.Source
......@@ -37,7 +36,7 @@ object AclSyncJob {
final val TAG = "AclSyncJob"
def schedule(route: String): Int = new JobRequest.Builder(AclSyncJob.TAG + ':' + route)
.setExecutionWindow(1, TimeUnit.DAYS.toMillis(28))
.setExecutionWindow(TimeUnit.SECONDS.toMillis(10), TimeUnit.DAYS.toMillis(28))
.setRequirementsEnforced(true)
.setRequiredNetworkType(JobRequest.NetworkType.UNMETERED)
.setRequiresCharging(true)
......@@ -47,11 +46,10 @@ object AclSyncJob {
class AclSyncJob(route: String) extends Job {
override def onRunJob(params: Params): Result = {
val filename = route + ".acl"
try {
//noinspection JavaAccessorMethodCalledAsEmptyParen
IOUtils.writeString(app.getApplicationInfo.dataDir + '/' + filename,
Source.fromURL("https://shadowsocks.org/acl/android/v1/" + filename).mkString)
IOUtils.writeString(Acl.getFile(route),
Source.fromURL("https://shadowsocks.org/acl/android/v1/" + route + ".acl").mkString)
Result.SUCCESS
} catch {
case e: IOException =>
......
......@@ -20,6 +20,7 @@ import com.github.shadowsocks.widget.UndoSnackbarManager
import com.github.shadowsocks.{MainActivity, R, ToolbarFragment}
import scala.collection.mutable
import scala.io.Source
/**
* @author Mygod
......@@ -63,9 +64,8 @@ class CustomRulesFragment extends ToolbarFragment with Toolbar.OnMenuItemClickLi
}
private val selectedItems = new mutable.HashSet[AnyRef]
private def onSelectedItemsUpdated(): Unit = if (selectionItem != null) {
selectionItem.setVisible(selectedItems.nonEmpty)
}
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 {
......@@ -145,8 +145,12 @@ class CustomRulesFragment extends ToolbarFragment with Toolbar.OnMenuItemClickLi
apply()
index
} else -1
def addToProxy(input: String): Int = try addSubnet(Subnet.fromString(input)) catch {
case _: IllegalArgumentException => addHostname(input)
def addToProxy(input: String): Int = {
val acl = new Acl().fromSource(Source.fromString(input), defaultBypass = true)
var result = -1
for (hostname <- acl.proxyHostnames) result = addHostname(hostname)
if (acl.bypass) for (subnet <- acl.subnets) result = addSubnet(subnet)
result
}
def addFromTemplate(template: Int, text: CharSequence): Int = template match {
case GENERIC => addToProxy(text.toString)
......@@ -263,6 +267,13 @@ class CustomRulesFragment extends ToolbarFragment with Toolbar.OnMenuItemClickLi
}).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)
......@@ -292,13 +303,7 @@ class CustomRulesFragment extends ToolbarFragment with Toolbar.OnMenuItemClickLi
.create().show()
true
case R.id.action_import =>
try {
val items = clipboard.getPrimaryClip.getItemAt(0).getText.toString.split("\n")
if (items.nonEmpty) {
items.foreach(adapter.addToProxy)
return true
}
} catch {
try if (adapter.addToProxy(clipboard.getPrimaryClip.getItemAt(0).getText.toString) != -1) return true catch {
case _: Exception =>
}
Snackbar.make(getActivity.findViewById(R.id.snackbar), R.string.action_import_err, Snackbar.LENGTH_LONG).show()
......
package com.github.shadowsocks.acl
import java.net.{Inet4Address, Inet6Address, InetAddress}
import java.net.InetAddress
import java.util.Objects
import com.github.shadowsocks.utils.Utils
......@@ -9,16 +10,11 @@ import com.github.shadowsocks.utils.Utils
*/
@throws[IllegalArgumentException]
class Subnet(val address: InetAddress, val prefixSize: Int) extends Comparable[Subnet] {
if (prefixSize < 0) throw new IllegalArgumentException
address match {
case _: Inet4Address => if (prefixSize > 32) throw new IllegalArgumentException
case _: Inet6Address => if (prefixSize > 128) throw new IllegalArgumentException
}
private def addressLength = address.getAddress.length << 3
if (prefixSize < 0 || prefixSize > addressLength) throw new IllegalArgumentException
override def toString: String = if (address match {
case _: Inet4Address => prefixSize == 32
case _: Inet6Address => prefixSize == 128
}) address.getHostAddress else address.getHostAddress + '/' + prefixSize
override def toString: String =
if (prefixSize == addressLength) address.getHostAddress else address.getHostAddress + '/' + prefixSize
override def compareTo(that: Subnet): Int = {
val addrThis = address.getAddress
......@@ -31,19 +27,21 @@ class Subnet(val address: InetAddress, val prefixSize: Int) extends Comparable[S
}
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("/")
if (!Utils.isNumeric(parts(0))) throw new IllegalArgumentException()
val addr = InetAddress.getByName(parts(0))
val addr = Utils.parseNumericAddress(parts(0))
parts.length match {
case 1 => new Subnet(addr, addr match {
case _: Inet4Address => 32
case _: Inet6Address => 128
})
case 1 => new Subnet(addr, addr.getAddress.length << 3)
case 2 => new Subnet(addr, parts(1).toInt)
case _ => throw new IllegalArgumentException()
}
......
......@@ -46,7 +46,7 @@ object DBHelper {
}
class DBHelper(val context: Context)
extends OrmLiteSqliteOpenHelper(context, DBHelper.PROFILE, null, 21) {
extends OrmLiteSqliteOpenHelper(context, DBHelper.PROFILE, null, 22) {
import DBHelper._
lazy val profileDao: Dao[Profile, Int] = getDao(classOf[Profile])
......@@ -77,9 +77,6 @@ class DBHelper(val context: Context)
} else if (oldVersion < 19) {
profileDao.executeRawNoArgs("UPDATE `profile` SET route = 'all' WHERE route IS NULL;")
}
if (oldVersion < 10) {
profileDao.executeRawNoArgs("ALTER TABLE `profile` ADD COLUMN auth SMALLINT;")
}
if (oldVersion < 11) {
profileDao.executeRawNoArgs("ALTER TABLE `profile` ADD COLUMN ipv6 SMALLINT;")
}
......@@ -89,9 +86,9 @@ class DBHelper(val context: Context)
TableUtils.createTable(connectionSource, classOf[Profile])
profileDao.executeRawNoArgs(
"INSERT INTO `profile`(id, name, host, localPort, remotePort, password, method, route, proxyApps, bypass," +
" udpdns, auth, ipv6, individual) " +
"SELECT id, name, host, localPort, remotePort, password, method, route, 1 - global, bypass, udpdns, auth," +
" ipv6, individual FROM `tmp`;")
" 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) {
......@@ -115,24 +112,26 @@ class DBHelper(val context: Context)
profileDao.executeRawNoArgs("UPDATE `profile` SET route = 'bypass-lan-china' WHERE route = 'bypass-china'")
}
if (oldVersion < 17) {
profileDao.executeRawNoArgs("ALTER TABLE `profile` ADD COLUMN kcp SMALLINT;")
profileDao.executeRawNoArgs("ALTER TABLE `profile` ADD COLUMN kcpcli VARCHAR DEFAULT " +
"'--crypt none --mode normal --mtu 1200 --nocomp --dscp 46 --parityshard 0';")
} else if (oldVersion < 20) {
profileDao.executeRawNoArgs("UPDATE `profile` SET kcpcli = '--crypt none --mode normal --mtu 1200 --nocomp " +
"--dscp 46 --parityshard 0' WHERE kcpcli IS NULL;")
}
if (oldVersion < 18) {
profileDao.executeRawNoArgs("ALTER TABLE `profile` ADD COLUMN kcpPort INTEGER DEFAULT 8399;")
} else if (oldVersion < 19) {
profileDao.executeRawNoArgs("UPDATE `profile` SET kcpPort = 8399 WHERE kcpPort = 0;")
}
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;")
}
} catch {
case ex: Exception =>
app.track(ex)
......
......@@ -20,13 +20,13 @@
package com.github.shadowsocks.database
import java.net.URLEncoder
import java.util.Locale
import android.content.SharedPreferences
import android.net.Uri
import android.os.Binder
import android.text.TextUtils
import android.util.Base64
import com.github.shadowsocks.plugin.PluginConfiguration
import com.github.shadowsocks.utils.Key
import com.j256.ormlite.field.{DataType, DatabaseField}
......@@ -68,9 +68,6 @@ class Profile {
@DatabaseField
var udpdns: Boolean = _
@DatabaseField
var auth: Boolean = _
@DatabaseField
var ipv6: Boolean = _
......@@ -90,23 +87,25 @@ class Profile {
var userOrder: Long = _
@DatabaseField
var kcp: Boolean = _
@DatabaseField
var kcpPort: Int = 8399
@DatabaseField
var kcpcli: String = "--crypt none --mode normal --mtu 1200 --nocomp --dscp 46 --parityshard 0"
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
override def toString: String = "ss://" + Base64.encodeToString("%s%s:%s@%s:%d".formatLocal(Locale.ENGLISH,
method, if (auth) "-auth" else "", password, host, remotePort).getBytes, Base64.NO_PADDING | Base64.NO_WRAP) +
(if (TextUtils.isEmpty(name)) "" else '#' + URLEncoder.encode(name, "utf-8"))
def isMethodUnsafe: Boolean = "table".equalsIgnoreCase(method) || "rc4".equalsIgnoreCase(method)
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), 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(editor: SharedPreferences.Editor): SharedPreferences.Editor = editor
.putString(Key.name, name)
......@@ -120,12 +119,9 @@ class Profile {
.putBoolean(Key.proxyApps, proxyApps)
.putBoolean(Key.bypass, bypass)
.putBoolean(Key.udpdns, udpdns)
.putBoolean(Key.auth, auth)
.putBoolean(Key.ipv6, ipv6)
.putString(Key.individual, individual)
.putBoolean(Key.kcp, kcp)
.putInt(Key.kcpPort, kcpPort)
.putString(Key.kcpcli, kcpcli)
.putString(Key.plugin, plugin)
.remove(Key.dirty)
def deserialize(pref: SharedPreferences) {
// It's assumed that default values are never used, so 0/false/null is always used even if that isn't the case
......@@ -140,11 +136,8 @@ class Profile {
proxyApps = pref.getBoolean(Key.proxyApps, false)
bypass = pref.getBoolean(Key.bypass, false)
udpdns = pref.getBoolean(Key.udpdns, false)
auth = pref.getBoolean(Key.auth, false)
ipv6 = pref.getBoolean(Key.ipv6, false)
individual = pref.getString(Key.individual, null)
kcp = pref.getBoolean(Key.kcp, false)
kcpPort = pref.getInt(Key.kcpPort, 0)
kcpcli = pref.getString(Key.kcpcli, null)
plugin = pref.getString(Key.plugin, null)
}
}
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.getPackageManager.getPackageInfo(app.getPackageName, PackageManager.GET_SIGNATURES).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 = packageManager.getPackageInfo(packageName, PackageManager.GET_SIGNATURES)
.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.v14.preference.PreferenceDialogFragment
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 PreferenceDialogFragment {
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(android.R.id.text1).asInstanceOf[TextView]
private val text2 = view.findViewById(android.R.id.text2).asInstanceOf[TextView]
private val icon = view.findViewById(android.R.id.icon).asInstanceOf[ImageView]
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
import be.mygod.preference.DialogPreferencePlus
/**
* @author Mygod
*/
class IconListPreference(context: Context, attrs: AttributeSet = null) extends ListPreference(context, attrs)
with DialogPreferencePlus {
override def createDialog() = new BottomSheetPreferenceDialogFragment()
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()
}
}
......@@ -18,37 +18,29 @@
/* */
/*******************************************************************************/
package com.github.shadowsocks.preferences
package com.github.shadowsocks.preference
import android.app.AlertDialog
import android.content.DialogInterface
import eu.chainfire.libsuperuser.Shell
import be.mygod.preference.EditTextPreferenceDialogFragment
import scala.collection.JavaConverters._
import com.github.shadowsocks.ProfileConfigActivity
import com.github.shadowsocks.plugin.{PluginContract, PluginManager}
/**
* @author Mygod
*/
class KcpCliPreferenceDialogFragment extends EditTextPreferenceDialogFragment {
object PluginConfigurationDialogFragment {
final val PLUGIN_ID_FRAGMENT_TAG = "com.github.shadowsocks.preference.PluginConfigurationDialogFragment.PLUGIN_ID"
}
class PluginConfigurationDialogFragment extends EditTextPreferenceDialogFragment {
import PluginConfigurationDialogFragment._
override def onPrepareDialogBuilder(builder: AlertDialog.Builder) {
super.onPrepareDialogBuilder(builder)
builder.setNeutralButton("?", ((_, _) => new AlertDialog.Builder(builder.getContext)
.setTitle("?")
.setMessage(Shell.SH.run(builder.getContext.getApplicationInfo.nativeLibraryDir + "/libkcptun.so --help")
.asScala
.dropWhile(line => line != "GLOBAL OPTIONS:")
.drop(1)
.takeWhile(line => line.length() > 3)
.filter(line =>
!line.startsWith(" --localaddr ") &&
!line.startsWith(" --remoteaddr ") &&
!line.startsWith(" --path ") &&
!line.startsWith(" --help,") &&
!line.startsWith(" --version,"))
.mkString("\n")
.replaceAll(" {2,}", "\n")
.substring(1)) // remove 1st \n
.show()): DialogInterface.OnClickListener)
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)
}
}
......@@ -26,13 +26,11 @@ object Executable {
val SS_LOCAL = "ss-local"
val SS_TUNNEL = "ss-tunnel"
val TUN2SOCKS = "tun2socks"
val KCPTUN = "kcptun"
val EXECUTABLES = Array(SS_LOCAL, SS_TUNNEL, PDNSD, REDSOCKS, TUN2SOCKS, KCPTUN)
val EXECUTABLES = Array(SS_LOCAL, SS_TUNNEL, PDNSD, REDSOCKS, TUN2SOCKS)
}
object ConfigUtils {
val SHADOWSOCKS = "{\"server\": \"%s\", \"server_port\": %d, \"local_port\": %d, \"password\": \"%s\", \"method\":\"%s\", \"timeout\": %d}"
val REDSOCKS = "base {\n" +
" log_debug = off;\n" +
" log_info = off;\n" +
......@@ -143,7 +141,6 @@ object Key {
val proxyApps = "isProxyApps"
val bypass = "isBypassApps"
val udpdns = "isUdpDns"
val auth = "isAuth"
val ipv6 = "isIpv6"
val host = "proxy"
......@@ -153,9 +150,8 @@ object Key {
val localPort = "localPortNum"
val remoteDns = "remoteDns"
val kcp = "kcp"
val kcpPort = "kcpPort"
val kcpcli = "kcpcli"
val plugin = "plugin"
val pluginConfigure = "plugin.configure"
val dirty = "profileDirty"
......
......@@ -21,7 +21,7 @@
package com.github.shadowsocks.utils
import com.github.shadowsocks.utils.CloseUtils._
import java.io.{FileWriter, InputStream, OutputStream}
import java.io._
/**
* @author Mygod
......@@ -37,6 +37,14 @@ object IOUtils {
}
}
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)
}
}
......@@ -20,32 +20,49 @@
package com.github.shadowsocks.utils
import java.net.URLDecoder
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+-/=_]+)(#(.+))?".r
private val decodedPattern = "(?i)^((.+?)(-auth)??:(.*)@(.+?):(\\d+?))$".r
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 => try
decodedPattern.findFirstMatchIn(new String(Base64.decode(m.group(1), Base64.NO_PADDING), "UTF-8")) match {
case Some(ss) =>
val profile = new Profile
profile.method = ss.group(2).toLowerCase
if (ss.group(3) != null) profile.auth = true
profile.password = ss.group(4)
profile.host = ss.group(5)
profile.remotePort = ss.group(6).toInt
if (m.group(2) != null) profile.name = URLDecoder.decode(m.group(3), "utf-8")
profile
case _ => null
} catch {
case ex: Exception =>
Log.e(TAG, "parser error: " + m.source, ex)// Ignore
null
}).filter(_ != null)
pattern.findAllMatchIn(if (data == null) "" else data).map(m => {
val uri = Uri.parse(m.matched)
uri.getUserInfo match {
case null => new String(Base64.decode(uri.getHost, Base64.NO_PADDING), "UTF-8") 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
profile.host = uri.getHost
profile.remotePort = uri.getPort
profile.plugin = uri.getQueryParameter(Key.plugin)
profile.name = uri.getFragment
profile
case _ =>
Log.e(TAG, "Unknown user info: " + m.matched)
null
}
}
}).filter(_ != null)
}
......@@ -25,6 +25,7 @@ import java.io.File
import eu.chainfire.libsuperuser.Shell
import scala.collection.JavaConverters._
import scala.io.Source
/**
* @author Mygod
......@@ -45,7 +46,7 @@ object TcpFastOpen {
def sendEnabled: Boolean = {
val file = new File("/proc/sys/net/ipv4/tcp_fastopen")
file.canRead && (Utils.readAllLines(file).toInt & 1) > 0
file.canRead && (Source.fromFile(file).mkString.trim.toInt & 1) > 0
}
def enabled(value: Boolean): String = if (sendEnabled != value) {
......
......@@ -45,7 +45,7 @@ object TrafficMonitor {
def formatTraffic(size: Long): String = {
var n: Double = size
var i = -1
while (n >= 1000) {
while (n >= 999.5) {
n /= 1024
i = i + 1
}
......
......@@ -32,7 +32,7 @@ import com.github.shadowsocks.ShadowsocksApplication.app
class TrafficMonitorThread(context: Context) extends Thread {
val TAG = "TrafficMonitorThread"
lazy val PATH: String = context.getApplicationInfo.dataDir + "/stat_path"
val stat = new File(context.getFilesDir, "/stat_path")
@volatile var serverSocket: LocalServerSocket = _
@volatile var isRunning: Boolean = true
......@@ -55,11 +55,11 @@ class TrafficMonitorThread(context: Context) extends Thread {
override def run() {
new File(PATH).delete()
stat.delete()
try {
val localSocket = new LocalSocket
localSocket.bind(new LocalSocketAddress(PATH, LocalSocketAddress.Namespace.FILESYSTEM))
localSocket.bind(new LocalSocketAddress(stat.getAbsolutePath, LocalSocketAddress.Namespace.FILESYSTEM))
serverSocket = new LocalServerSocket(localSocket.getFileDescriptor)
} catch {
case e: IOException =>
......
......@@ -20,12 +20,10 @@
package com.github.shadowsocks.utils
import java.io.File
import java.lang.reflect.InvocationTargetException
import java.net._
import java.security.MessageDigest
import java.util.{Scanner, StringTokenizer}
import android.animation.{Animator, AnimatorListenerAdapter}
import android.content.pm.PackageManager
import android.content.{Context, Intent}
import android.graphics._
......@@ -39,7 +37,6 @@ import com.github.shadowsocks.{BuildConfig, ShadowsocksRunnerService}
import org.xbill.DNS._
import scala.collection.JavaConversions._
import scala.collection.mutable.ArrayBuffer
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.Future
import scala.util.{Failure, Try}
......@@ -58,8 +55,6 @@ object Utils {
new String(Base64.encode(mdg.digest, 0))
}
def dpToPx(context: Context, dp: Int): Int = Math.round(dp * context.getResources.getDisplayMetrics.density)
/*
* round or floor depending on whether you are using offsets(floor) or
* widths(round)
......@@ -82,64 +77,6 @@ object Utils {
toast
}
def readAllLines(f: File): String = {
val scanner = new Scanner(f)
try {
scanner.useDelimiter("\\Z")
scanner.next()
} finally scanner.close()
}
def printToFile(f: java.io.File)(op: java.io.PrintWriter => Unit) {
val p = new java.io.PrintWriter(f)
try {
op(p)
} finally {
p.close()
}
}
/**
* Crack a command line.
* Based on: https://github.com/apache/ant/blob/588ce1f/src/main/org/apache/tools/ant/types/Commandline.java#L471
* @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 ArrayBuffer.
*/
def translateCommandline(toProcess: String): ArrayBuffer[String] = {
if (toProcess == null || toProcess.length == 0) return ArrayBuffer[String]()
val tok = new StringTokenizer(toProcess, "\"' ", true)
val result = ArrayBuffer[String]()
val current = new StringBuilder()
var quote = ' '
var last = " "
while (tok.hasMoreTokens) {
val nextTok = tok.nextToken
quote match {
case '\'' => nextTok match {
case "'" => quote = ' '
case _ => current.append(nextTok)
}
case '"' => nextTok match {
case "\"" => quote = ' '
case _ => current.append(nextTok)
}
case _ => nextTok match {
case "'" => quote = '\''
case "\"" => quote = '"'
case " " => if (last != " ") {
result.append(current.toString)
current.setLength(0)
}
case _ => current.append(nextTok)
}
}
last = nextTok
}
if (current.nonEmpty) result.append(current.toString)
if (quote == '\'' || quote == '"') throw new Exception("Unbalanced quotes in " + toProcess)
result
}
def resolve(host: String, addrType: Int): Option[String] = {
try {
val lookup = new Lookup(host, addrType)
......@@ -172,25 +109,29 @@ object Utils {
.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 {
for (intf <- enumerationAsScalaIterator(NetworkInterface.getNetworkInterfaces))
for (addr <- enumerationAsScalaIterator(intf.getInetAddresses))
if (addr.isInstanceOf[Inet6Address] && !addr.isLoopbackAddress && !addr.isLinkLocalAddress) {
if (BuildConfig.DEBUG) Log.d(TAG, "IPv6 address detected")
return true
}
} catch {
case ex: Exception =>
Log.e(TAG, "Failed to get interfaces' addresses.", ex)
app.track(ex)
}
false
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) {
......
* 0.0.2:
* Add `getOrDefault` to `PluginOptions`;
* Update support library version to 25.1.1.
* 0.0.1: Initial release.
# shadowsocks-android plugin framework
[Documentation](doc.md) | [Change log](CHANGES.md)
Support library for easier development on [shadowsocks
plugin](https://github.com/shadowsocks/shadowsocks-org/issues/28) for Android. Also includes some
useful resources to easily get consistent styling with the main app.
## Official plugins
These are some plugins ready to use on shadowsocks-android.
* [simple-obfs](https://github.com/shadowsocks/simple-obfs-android/releases)
* [kcptun](https://github.com/shadowsocks/kcptun-android/releases)
## Developer's guide
WARNING: This library is still in beta (0.x) and its content is subject to massive changes.
This library is designed with Java interoperability in mind so theoretically you can use this
library with other languages and/or build tools but there isn't documentation for that yet. This
guide is written for Scala + SBT. Contributions are welcome.
### Package name
There are no arbitrary restrictions/requirements on package name, component name and content
provider authority, but you're suggested to follow the format in this documentations. For package
name, use `com.github.shadowsocks.plugin.$PLUGIN_ID` if it only contains a single plugin to
prevent duplicated plugins. In some places hyphens are not accepted, for example package name. In
that case, hyphens `-` should be changed into underscores `_`. For example, the package name for
`obfs-local` would probably be `com.github.shadowsocks.plugin.obfs_local`.
### Add dependency
First you need to add this library to your dependencies. This library is written mostly in Scala
and it's most convenient to use it with SBT:
```scala
libraryDependencies += "com.github.shadowsocks" %% "plugin" % "0.0.2"
```
### Native binary configuration
First you need to get your native binary compiling on Android platform.
* [Sample project for C](https://github.com/shadowsocks/simple-obfs-android/tree/4f82c4a4e415d666e70a7e2e60955cb0d85c1615);
* [Sample project for Go](https://github.com/shadowsocks/kcptun-android/tree/41f42077e177618553417c16559784a51e9d8c4c).
In addition to functionalities of a normal plugin, it has to support these additional flags that
may get passed through arguments:
* `-V`: VPN mode. In this case, the plugin should pass all file descriptors that needs protecting
from VPN connections (i.e. its traffic will not be forwarded through the VPN) through an
ancillary message to `./protect_path`;
* `--fast-open`: TCP fast open enabled.
### Implement a binary provider
It's super easy. You just need to implement two or three methods. For example for `obfs-local`:
```scala
final class BinaryProvider extends NativePluginProvider {
override protected def populateFiles(provider: PathProvider) {
provider.addPath("obfs-local", "755")
// add additional files here
}
// remove this method to disable fast mode, read more in the documentation
override def getExecutable: String =
getContext.getApplicationInfo.nativeLibraryDir + "/libobfs-local.so"
override def openFile(uri: Uri): ParcelFileDescriptor = uri.getPath match {
case "/obfs-local" =>
ParcelFileDescriptor.open(new File(getExecutable), ParcelFileDescriptor.MODE_READ_ONLY)
// handle additional files here
case _ => throw new FileNotFoundException()
}
}
```
Then add it to your manifest:
```xml
<manifest>
...
<application>
...
<provider android:name=".BinaryProvider"
android:exported="true"
android:authorities="$FULLY_QUALIFIED_NAME_OF_YOUR_CONTENTPROVIDER">
<intent-filter>
<action android:name="com.github.shadowsocks.plugin.ACTION_NATIVE_PLUGIN"/>
</intent-filter>
<intent-filter>
<action android:name="com.github.shadowsocks.plugin.ACTION_NATIVE_PLUGIN"/>
<data android:scheme="plugin"
android:host="com.github.shadowsocks"
android:pathPrefix="/$PLUGIN_ID"/>
</intent-filter>
<meta-data android:name="com.github.shadowsocks.plugin.id"
android:value="$PLUGIN_ID"/>
<meta-data android:name="com.github.shadowsocks.plugin.default_config"
android:value="dummy=default;plugin=options"/>
</provider>
...
</application>
</manifest>
```
### Add user interfaces
You should add to your plugin app a configuration activity or a help activity or both if you're
going to use `ConfigurationActivity.fallbackToManualEditor`.
#### Configuration activity
This is used if found instead of a manual input dialog when user clicks "Configure..." in the main
app. This gives you maximum freedom of the user interface. To implement this, you need to extend
`ConfigurationActivity` and you will get current options via
`onInitializePluginOptions(PluginOptions)` and you can invoke `saveChanges(PluginOptions)` or
`discardChanges()` before `finish()` or `fallbackToManualEditor()`. Then add it to your manifest:
```xml
<manifest>
...
<application>
...
<activity android:name=".ConfigActivity">
<intent-filter>
<action android:name="com.github.shadowsocks.plugin.ACTION_CONFIGURE"/>
<category android:name="android.intent.category.DEFAULT"/>
<data android:scheme="plugin"
android:host="com.github.shadowsocks"
android:path="/$PLUGIN_ID"/>
</intent-filter>
</activity>
...
</application>
</manifest>
```
#### Help activity/callback
This is started when user taps "?" in manual editor. To implement this, you need to extend
`HelpCallback` if you want a simple dialog with help message as `CharSequence` or `HelpActivity`
if you want to provide custom user interface, implement the required methods, then add it to your
manifest:
```xml
<manifest>
...
<application>
...
<activity android:name=".HelpActivity">
<intent-filter>
<action android:name="com.github.shadowsocks.plugin.ACTION_HELP"/>
<category android:name="android.intent.category.DEFAULT"/>
<data android:scheme="plugin"
android:host="com.github.shadowsocks"
android:path="/$PLUGIN_ID"/>
</intent-filter>
</activity>
...
</application>
</manifest>
```
Great. Now your plugin is ready to use.
enablePlugins(AndroidLib)
android.useSupportVectors
name := "plugin"
version := "0.0.2"
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>
}
# Overview
Plugin should be bundled as an apk. `$PLUGIN_ID` in this documentation corresponds to the
executable name for the plugin in order to be cross-platform, e.g. `obfs-local`. An apk can have
more than one plugins bundled. We don't care as long as they have different `$PLUGIN_ID`. For
duplicated plugin ID, host should refuse to start.
There are no arbitrary restrictions/requirements on package name, component name and content
provider authority, but you're suggested to follow the format in this documentations. For package
name, use `com.github.shadowsocks.plugin.$PLUGIN_ID` if it only contains a single plugin to prevent
duplicated plugins. In some places hyphens are not accepted, for example package name. In that
case, hyphens `-` should be changed into underscores `_`. For example, the package name for
`obfs-local` would probably be `com.github.shadowsocks.plugin.obfs_local`.
It's advised to use this library for easier development, but you're free to start from scratch following this
documentation.
# Plugin configuration
Plugins get their args configured via one of the following two options:
* A configuration activity;
([example](https://github.com/shadowsocks/simple-obfs-android/tree/4f82c4a4e415d666e70a7e2e60955cb0d85c1615))
* If no configuration activity is found or the activity requests the fallback mode, the fallback
mode will be used: user manual input and optional help message.
([example](https://github.com/shadowsocks/kcptun-android/tree/41f42077e177618553417c16559784a51e9d8c4c))
Your user interface need not be consistent with shadowsocks-android styling - you don't need to use
preferences UI at all if you don't feel like it - however it's recommended to use Material Design
at minimum.
## Configuration activity
If the plugin provides a configuration activity, it will be started when user picks your plugin and
taps configure. It:
* MUST have action: `com.github.shadowsocks.plugin.ACTION_CONFIGURE`;
* MUST have category: `android.intent.category.DEFAULT`;
* MUST be able to receive data URI `plugin://com.github.shadowsocks/$PLUGIN_ID`;
* SHOULD parse string extra `com.github.shadowsocks.plugin.EXTRA_OPTIONS` (all options as a single
string) and display the current options;
* SHOULD distinguish between server settings and feature settings in some way, e.g. for
`obfs-local`, `obfs` is a server setting and `obfs_host` is a feature setting;
* On finish, it SHOULD return one of the following results:
- `RESULT_OK = 0`: In this case it MUST return the data Intent with the new
`com.github.shadowsocks.plugin.EXTRA_OPTIONS`;
- `RESULT_CANCELED = -1`: Nothing will be changed;
- `RESULT_FALLBACK = 1`: Fallback mode is requested and the host should display the fallback
editor.
This corresponds to `com.github.shadowsocks.plugin.ConfigurationActivity` in the plugin library.
Here's what a proper configuration activity usually should look like in `AndroidManifest.xml`:
```xml
<manifest>
...
<application>
...
<activity android:name=".ConfigActivity">
<intent-filter>
<action android:name="com.github.shadowsocks.plugin.ACTION_CONFIGURE"/>
<category android:name="android.intent.category.DEFAULT"/>
<data android:scheme="plugin"
android:host="com.github.shadowsocks"
android:path="/$PLUGIN_ID"/>
</intent-filter>
</activity>
...
</application>
</manifest>
```
## Help activity/callback
If the plugin doesn't provide a configuration activity, it's highly recommended to provide a help
message in the form of an Activity. It:
* MUST have action: `com.github.shadowsocks.plugin.ACTION_HELP`;
* MUST have category: `android.intent.category.DEFAULT`;
* MUST be able to receive data URI `plugin://com.github.shadowsocks/$PLUGIN_ID`;
* CAN parse string extra `com.github.shadowsocks.plugin.EXTRA_OPTIONS` and display some more
relevant information;
* SHOULD either:
- Be invisible and return help message with CharSequence extra
`com.github.shadowsocks.plugin.EXTRA_HELP_MESSAGE` in the data intent with `RESULT_OK`; (in this
case, a simple dialog will be shown containing the message)
- Be visible and return `RESULT_CANCELED`.
* SHOULD distinguish between server settings and feature settings in some way, e.g. for
`simple_obfs`, `obfs` is a server setting and `obfs_host` is a feature setting.
This corresponds to `com.github.shadowsocks.plugin.HelpActivity` or
`com.github.shadowsocks.plugin.HelpCallback` in the plugin library. Here's what a proper help
activity/callback usually should look like in `AndroidManifest.xml`:
```xml
<manifest>
...
<application>
...
<activity android:name=".HelpActivity">
<intent-filter>
<action android:name="com.github.shadowsocks.plugin.ACTION_HELP"/>
<category android:name="android.intent.category.DEFAULT"/>
<data android:scheme="plugin"
android:host="com.github.shadowsocks"
android:path="/$PLUGIN_ID"/>
</intent-filter>
</activity>
...
</application>
</manifest>
```
# Plugin implementation
Every plugin can be either in native mode or JVM mode.
## Native mode
In native mode, plugins are provided as native executables and `shadowsocks-libev`'s plugin mode
will be used.
Every native mode plugin MUST have a content provider to provide the native executables (since they
can exceed 1M which is the limit of Intent size) that:
* MUST have `android:label` and `android:icon`; (may be configured by its parent `application`)
* MUST have an intent filter with action `com.github.shadowsocks.plugin.ACTION_NATIVE_PLUGIN`;
(used for discovering plugins)
* MUST have meta-data `com.github.shadowsocks.plugin.id` with string value `$PLUGIN_ID`;
* MUST have an intent filter with action `com.github.shadowsocks.plugin.ACTION_NATIVE_PLUGIN` and
data `plugin://com.github.shadowsocks/$PLUGIN_ID`; (used for configuring plugin)
* CAN have meta-data `com.github.shadowsocks.plugin.default_config` with string value, default is
empty;
* MUST implement `query` that returns the file list which MUST include `$PLUGIN_ID` when having
these as arguments:
- `uri = "content://$authority_of_your_provider`;
- `projection = ["path", "mode"]`; (relative path, for example `obfs-local`; file mode, for
example `755`)
- `selection = null`;
- `selectionArgs = null`;
- `sortOrder = null`;
* MUST implement `openFile` that for files returned in `query`, `openFile` with `mode = "r"` returns
a valid `ParcelFileDescriptor` for reading. For example, `uri` can be
`content://com.github.shadowsocks.plugin.kcptun/kcptun`.
This corresponds to `com.github.shadowsocks.plugin.NativePluginProvider` in the plugin library.
Here's what a proper native plugin provider usually should look like in `AndroidManifest.xml`:
```xml
<manifest>
...
<application>
...
<provider android:name=".BinaryProvider"
android:exported="true"
android:authorities="$FULLY_QUALIFIED_NAME_OF_YOUR_CONTENTPROVIDER">
<intent-filter>
<action android:name="com.github.shadowsocks.plugin.ACTION_NATIVE_PLUGIN"/>
</intent-filter>
<intent-filter>
<action android:name="com.github.shadowsocks.plugin.ACTION_NATIVE_PLUGIN"/>
<data android:scheme="plugin"
android:host="com.github.shadowsocks"
android:pathPrefix="/$PLUGIN_ID"/>
</intent-filter>
<meta-data android:name="com.github.shadowsocks.plugin.id"
android:value="$PLUGIN_ID"/>
<meta-data android:name="com.github.shadowsocks.plugin.default_config"
android:value="dummy=default;plugin=options"/>
</provider>
...
</application>
</manifest>
```
## Native mode without binary copying
If your plugin binary executable can run in place, you can support native mode without binary
copying. To support this mode, your `ContentProvider` must first support native mode with binary
copying (this will be used if the fast routine fails) and:
* MUST implement `call` that returns absolute path to the entry executable as
`com.github.shadowsocks.plugin.EXTRA_ENTRY` when having `method = "shadowsocks:getExecutable"`;
(`com.github.shadowsocks.plugin.EXTRA_OPTIONS` is provided in extras as well just in case you
need them)
If you don't plan to support this mode, you can just throw `UnsupportedOperationException` when
being invoked. It will fallback to the slow routine automatically.
## JVM mode
This feature hasn't been implemented yet.
# Plugin security
Plugins are certified using package signatures and shadowsocks-android will consider these
signatures as trusted:
* Signatures by [trusted sources](/mobile/src/main/scala/com/github/shadowsocks/plugin/PluginManager.scala#L22)
which includes:
- @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
signatures to this trusted sources if they have plugins signed by them before publishing their
source code.
* Current package signature, which means:
- If you get apk from shadowsocks-android releases or Google Play, this means only apk signed by
@madeye will be recognized as trusted.
- If you get apk from a third-party fork, all plugins from that developer will get recognized as
trusted automatically even if its source code isn't available anywhere online.
A warning will be shown for untrusted plugins. No arbitrary restrictions will be applied.
# Plugin platform versioning
In order to be able to identify compatible and incompatible plugins, [Semantic
Versioning](http://semver.org/) will be used.
>Given a version number MAJOR.MINOR.PATCH, increment the:
>
>1. MAJOR version when you make incompatible API changes,
>2. MINOR version when you add functionality in a backwards-compatible manner, and
>3. PATCH version when you make backwards-compatible bug fixes.
Plugin app must include this in their application tag: (which should be automatically included if
you are using our library)
```
<meta-data android:name="com.github.shadowsocks.plugin.version"
android:value="0.0.2"/>
```
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.github.shadowsocks.plugin">
<uses-sdk
android:minSdkVersion="19"
android:targetSdkVersion="25"/>
<application
android:theme="@style/Theme.Shadowsocks">
<meta-data android:name="com.github.shadowsocks.plugin.version"
android:value="0.0.2"/>
</application>
</manifest>
package com.github.shadowsocks.plugin;
/**
* 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.
*
* @author Mygod
*/
public final class PluginContract {
private PluginContract() { }
/**
* ContentProvider Action: Used for NativePluginProvider.
*
* Constant Value: "com.github.shadowsocks.plugin.ACTION_NATIVE_PLUGIN"
*/
public static final String ACTION_NATIVE_PLUGIN = "com.github.shadowsocks.plugin.ACTION_NATIVE_PLUGIN";
/**
* Activity Action: Used for ConfigurationActivity.
*
* Constant Value: "com.github.shadowsocks.plugin.ACTION_CONFIGURE"
*/
public static final String ACTION_CONFIGURE = "com.github.shadowsocks.plugin.ACTION_CONFIGURE";
/**
* Activity Action: Used for HelpActivity or HelpCallback.
*
* Constant Value: "com.github.shadowsocks.plugin.ACTION_HELP"
*/
public static final String ACTION_HELP = "com.github.shadowsocks.plugin.ACTION_HELP";
/**
* The lookup key for a string that provides the plugin entry binary.
*
* Example: "/data/data/com.github.shadowsocks.plugin.obfs_local/lib/libobfs-local.so"
*
* Constant Value: "com.github.shadowsocks.plugin.EXTRA_ENTRY"
*/
public static final String EXTRA_ENTRY = "com.github.shadowsocks.plugin.EXTRA_ENTRY";
/**
* The lookup key for a string that provides the options as a string.
*
* Example: "obfs=http;obfs-host=www.baidu.com"
*
* Constant Value: "com.github.shadowsocks.plugin.EXTRA_OPTIONS"
*/
public static final String EXTRA_OPTIONS = "com.github.shadowsocks.plugin.EXTRA_OPTIONS";
/**
* The lookup key for a CharSequence that provides user relevant help message.
*
* Example: "obfs=<http|tls> Enable obfuscating: HTTP or TLS (Experimental).
* obfs-host=<host_name> Hostname for obfuscating (Experimental)."
*
* Constant Value: "com.github.shadowsocks.plugin.EXTRA_HELP_MESSAGE"
*/
public static final String EXTRA_HELP_MESSAGE = "com.github.shadowsocks.plugin.EXTRA_HELP_MESSAGE";
/**
* The metadata key to retrieve plugin id. Required for plugins.
*
* Constant Value: "com.github.shadowsocks.plugin.id"
*/
public static final String METADATA_KEY_ID = "com.github.shadowsocks.plugin.id";
/**
* The metadata key to retrieve default configuration. Default value is empty.
*
* Constant Value: "com.github.shadowsocks.plugin.default_config"
*/
public static final String METADATA_KEY_DEFAULT_CONFIG = "com.github.shadowsocks.plugin.default_config";
public static final String METHOD_GET_EXECUTABLE = "shadowsocks:getExecutable";
/** ConfigurationActivity result: fallback to manual edit mode. */
public static final int RESULT_FALLBACK = 1;
/**
* Relative to the file to be copied. This column is required.
*
* Example: "kcptun", "doc/help.txt"
*
* Type: String
*/
public static final String COLUMN_PATH = "path";
/**
* File mode bits. Default value is "644".
*
* Example: "755"
*
* Type: String
*/
public static final String COLUMN_MODE = "mode";
/**
* The scheme for general plugin actions.
*/
public static final String SCHEME = "plugin";
/**
* The authority for general plugin actions.
*/
public static final String AUTHORITY = "com.github.shadowsocks";
}
package com.github.shadowsocks.plugin;
import android.text.TextUtils;
import java.util.HashMap;
import java.util.Objects;
import java.util.StringTokenizer;
/**
* Helper class for processing plugin options.
*
* 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 PluginOptions() {
super();
}
public PluginOptions(int initialCapacity) {
super(initialCapacity);
}
public PluginOptions(int initialCapacity, float loadFactor) {
super(initialCapacity, loadFactor);
}
// TODO: this method is not needed since API 24
public String getOrDefault(Object key, String defaultValue) {
String v;
return (((v = get(key)) != null) || containsKey(key))
? v
: defaultValue;
}
private PluginOptions(String options, boolean parseId) {
this();
if (TextUtils.isEmpty(options)) return;
final StringTokenizer tokenizer = new StringTokenizer(options + ';', "\\=;", true);
final StringBuilder current = new StringBuilder();
String key = null;
while (tokenizer.hasMoreTokens()) {
String nextToken = tokenizer.nextToken();
if ("\\".equals(nextToken)) current.append(tokenizer.nextToken());
else if ("=".equals(nextToken) && key == null) {
key = current.toString();
current.setLength(0);
} else if (";".equals(nextToken)) {
if (key != null) {
put(key, current.toString());
key = null;
} else if (current.length() > 0)
if (parseId) id = current.toString(); else put(current.toString(), null);
current.setLength(0);
parseId = false;
} else current.append(nextToken);
}
}
public PluginOptions(String options) {
this(options, true);
}
public PluginOptions(String id, String options) {
this(options, false);
this.id = id;
}
public String id = "";
private static void append(StringBuilder result, String str) {
for (int i = 0; i < str.length(); ++i) {
char ch = str.charAt(i);
switch (ch) {
case '\\': case '=': case ';': result.append('\\'); // intentionally no break
default: result.append(ch);
}
}
}
public String toString(boolean trimId) {
final StringBuilder result = new StringBuilder();
if (!trimId) if (TextUtils.isEmpty(id)) return ""; else append(result, id);
for (Entry<String, String> entry : entrySet()) {
if (result.length() > 0) result.append(';');
append(result, entry.getKey());
String value = entry.getValue();
if (value != null) {
result.append('=');
append(result, value);
}
}
return result.toString();
}
@Override
public String toString() {
return toString(true);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
if (!super.equals(o)) return false;
PluginOptions that = (PluginOptions) o;
return Objects.equals(id, that.id) && super.equals(that);
}
@Override
public int hashCode() {
return Objects.hash(super.hashCode(), id);
}
}
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (C) 2015 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License
-->
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="48dp"
android:layout_marginBottom="48dp"
android:overScrollMode="ifContentScrolls">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="?attr/dialogPreferredPadding"
android:paddingEnd="?attr/dialogPreferredPadding"
android:orientation="vertical">
<TextView android:id="@android:id/message"
style="@style/TextAppearance.AppCompat.Subhead"
android:layout_marginBottom="48dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="?android:attr/textColorSecondary" />
<EditText
android:id="@android:id/edit"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>
</ScrollView>
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Theme.Material" parent="Theme.MaterialBase"/>
<style name="Theme.Shadowsocks" parent="Theme.ShadowsocksBase"/>
</resources>
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="NumberPickerPreference">
<attr name="max" format="integer" />
<attr name="min" format="integer" />
</declare-styleable>
</resources>
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="proxy_cat">Server Settings</string>
<string name="feature_cat">Feature Settings</string>
<string name="unsaved_changes_prompt">Changes not saved. Do you want to save?</string>
<string name="yes">Yes</string>
<string name="no">No</string>
<string name="apply">Apply</string>
</resources>
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Toolbar.Logo" parent="TextAppearance.Widget.AppCompat.Toolbar.Title">
<item name="android:textSize">24sp</item>
</style>
<style name="Theme.MaterialBase" parent="MaterialDrawerTheme.Light.DarkToolbar">
<style name="Theme.Shadowsocks.Material" parent="Theme.AppCompat.Light.NoActionBar"/>
<style name="Theme.ShadowsocksBase" parent="Theme.Shadowsocks.Material">
<!--suppress NewApi -->
<item name="android:navigationBarColor">@color/material_primary_700</item>
<item name="colorAccent">@color/material_accent_200</item>
<item name="colorButtonNormal">@color/material_accent_200</item>
<item name="colorPrimary">@color/material_primary_500</item>
<item name="colorPrimaryDark">@color/material_primary_700</item>
<item name="preferenceTheme">@style/Theme.Material.PreferenceThemeOverlay</item>
<item name="material_drawer_selected_text">@color/material_accent_200</item>
<item name="preferenceTheme">@style/Theme.Shadowsocks.PreferenceThemeOverlay</item>
</style>
<style name="Theme.Material.Dialog" parent="Theme.AppCompat.Light.Dialog">
<style name="Theme.Shadowsocks.Dialog" parent="Theme.AppCompat.Light.Dialog">
<item name="colorAccent">@color/material_primary_500</item>
<item name="colorButtonNormal">@color/material_accent_200</item>
<item name="colorPrimary">@color/material_primary_500</item>
<item name="colorPrimaryDark">@color/material_primary_700</item>
<item name="preferenceTheme">@style/Theme.Material.PreferenceThemeOverlay</item>
<item name="preferenceTheme">@style/Theme.Shadowsocks.PreferenceThemeOverlay</item>
<item name="windowNoTitle">true</item>
</style>
<!-- Fix for wrong padding on v17-v20 based on: https://github.com/Gericop/Android-Support-Preference-V7-Fix/blob/a6082cb/preference-v7/src/main/res/values-v17/styles.xml -->
<style name="Theme.Material" parent="Theme.MaterialBase">
<style name="Theme.Shadowsocks" parent="Theme.ShadowsocksBase">
<item name="android:listPreferredItemPaddingStart">16dp</item>
<item name="android:listPreferredItemPaddingEnd">16dp</item>
</style>
......@@ -39,20 +32,14 @@
<item name="android:textStyle">bold</item>
<item name="android:textColor">?android:attr/textColorPrimary</item>
</style>
<style name="Theme.Material.PreferenceThemeOverlay" parent="@style/PreferenceThemeOverlay.v14.Material">
<item name="preferenceFragmentListStyle">@style/Theme.Material.PreferenceFragmentList</item>
<style name="Theme.Shadowsocks.PreferenceThemeOverlay" parent="@style/PreferenceThemeOverlay.v14.Material">
<item name="preferenceFragmentListStyle">@style/Theme.Shadowsocks.PreferenceFragmentList</item>
</style>
<style name="Theme.Material.PreferenceFragmentList" parent="@style/PreferenceFragmentList.Material">
<style name="Theme.Shadowsocks.PreferenceFragmentList" parent="@style/PreferenceFragmentList.Material">
<item name="android:fadeScrollbars">true</item>
<item name="android:scrollbars">vertical</item>
</style>
<style name="NumberPickerStyle">
<item name="android:textSize">14sp</item>
</style>
<style name="StyledScrollerTextAppearance" parent="@android:style/TextAppearance">
<item name="android:textSize">24sp</item>
<item name="android:textStyle">bold</item>
<item name="android:textColor">@android:color/white</item>
</style>
</resources>
......@@ -25,6 +25,9 @@ import android.support.v7.preference.{EditTextPreference => Parent}
import android.support.v7.widget.AppCompatEditText
import android.text.InputType
import android.util.AttributeSet
import android.view.ViewGroup
import android.widget.FrameLayout
import com.github.shadowsocks.plugin.R
/**
* Fixed EditTextPreference + SummaryPreference with password support!
......@@ -35,6 +38,15 @@ class EditTextPreference(context: Context, attrs: AttributeSet = null) extends P
val editText = new AppCompatEditText(context, attrs)
editText.setId(android.R.id.edit)
{
val arr = context.obtainStyledAttributes(Array(R.attr.dialogPreferredPadding))
val margin = arr.getDimensionPixelOffset(0, 0)
arr.recycle()
val params = new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
params.setMargins(margin, 0, margin, 0)
editText.setLayoutParams(params)
}
override def createDialog() = new EditTextPreferenceDialogFragment()
override protected def getSummaryValue: String = {
......
......@@ -20,26 +20,28 @@
package be.mygod.preference
import android.content.Context
import android.support.v14.preference.PreferenceDialogFragment
import android.support.v7.widget.AppCompatEditText
import android.view.{View, ViewGroup}
class EditTextPreferenceDialogFragment extends PreferenceDialogFragment {
private lazy val preference = getPreference.asInstanceOf[EditTextPreference]
private lazy val editText = preference.editText
override protected def onCreateDialogView(context: Context): AppCompatEditText = {
val parent = editText.getParent.asInstanceOf[ViewGroup]
if (parent != null) parent.removeView(editText)
editText
}
protected lazy val editText: AppCompatEditText = preference.editText
override protected def onBindDialogView(view: View) {
super.onBindDialogView(view)
editText.setText(preference.getText)
val text = editText.getText
if (text != null) editText.setSelection(0, text.length)
val oldParent = editText.getParent.asInstanceOf[ViewGroup]
if (oldParent eq view) return
if (oldParent != null) oldParent.removeView(editText)
val oldEdit = view.findViewById(android.R.id.edit)
if (oldEdit == null) return
val container = oldEdit.getParent.asInstanceOf[ViewGroup]
if (container == null) return
container.removeView(oldEdit)
container.addView(editText, ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
}
override protected def needInputMethod = true
......
......@@ -26,7 +26,7 @@ import android.support.v7.preference.DialogPreference
import android.util.AttributeSet
import android.view.ContextThemeWrapper
import android.widget.NumberPicker
import com.github.shadowsocks.R
import com.github.shadowsocks.plugin.R
class NumberPickerPreference(private val context: Context, attrs: AttributeSet = null)
extends DialogPreference(context, attrs) with DialogPreferencePlus with SummaryPreference {
......
......@@ -30,9 +30,10 @@ abstract class PreferenceFragment extends Base {
override def onCreateView(inflater: LayoutInflater, container: ViewGroup, savedInstanceState: Bundle): View =
super.onCreateView(inflater, container, savedInstanceState)
protected final def displayPreferenceDialog(key: String, fragment: DialogFragment) {
protected final def displayPreferenceDialog(key: String, fragment: DialogFragment, other: Bundle = null) {
val bundle = new Bundle(1)
bundle.putString("key", key)
if (other != null) bundle.putAll(other)
fragment.setArguments(bundle)
fragment.setTargetFragment(this, 0)
getFragmentManager.beginTransaction()
......
......@@ -28,7 +28,7 @@ import android.support.v4.content.ContextCompat
import android.support.v4.view.ViewCompat
import android.support.v7.preference.{PreferenceGroup, PreferenceViewHolder, PreferenceGroupAdapter => Old}
import android.view.{LayoutInflater, View, ViewGroup}
import com.github.shadowsocks.R
import com.github.shadowsocks.plugin.R
/**
* Fix by: https://github.com/Gericop/Android-Support-Preference-V7-Fix/commit/7de016b007e28a264001a8bb353f110a7f64bb69
......
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.app.Activity
import android.content.Intent
import android.os.Bundle
import android.widget.Toast
/**
* Activity that's capable of getting EXTRA_OPTIONS input.
*
* @author Mygod
*/
trait OptionsCapableActivity extends Activity {
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
}
}
addSbtPlugin("org.scala-android" % "sbt-android" % "1.7.2")
addSbtPlugin("org.scala-android" % "sbt-android" % "1.7.4")
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
Subproject commit 3b4af24fecae932abe02830321af6f6cb54f72e2
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