diff --git a/.github/faq.md b/.github/faq.md index 75726121..848e607b 100644 --- a/.github/faq.md +++ b/.github/faq.md @@ -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? diff --git a/.github/issue_template.md b/.github/issue_template.md index d5edcba1..bc91020f 100644 --- a/.github/issue_template.md +++ b/.github/issue_template.md @@ -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 diff --git a/.gitignore b/.gitignore index 5cdc29d1..ecec4bea 100644 --- a/.gitignore +++ b/.gitignore @@ -1,33 +1,11 @@ -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 diff --git a/.gitmodules b/.gitmodules index 713a88a5..c14024db 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,35 +1,39 @@ -[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 diff --git a/.travis.yml b/.travis.yml index febc039b..62338c27 100644 --- a/.travis.yml +++ b/.travis.yml @@ -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 diff --git a/README.md b/README.md index 04564ceb..4799fdf4 100644 --- a/README.md +++ b/README.md @@ -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
  • shadowsocks-libev: GPLv3
  • pdnsd: GPLv3
  • libev: GPLv2
  • -
  • kcptun: MIT
  • libsodium: ISC
  • +
  • libudns: LGPL
  • ### LICENSE diff --git a/build.sbt b/build.sbt index 963ea618..bd13b300 100644 --- a/build.sbt +++ b/build.sbt @@ -1,71 +1,45 @@ -scalaVersion := "2.11.8" -dexMaxHeap := "4g" +lazy val commonSettings = Seq( + scalaVersion := "2.11.8", + dexMaxHeap := "4g", -enablePlugins(AndroidApp) -android.useSupportVectors + organization := "com.github.shadowsocks", -name := "shadowsocks" -version := "3.4.2" -versionCode := Some(174) + platformTarget := "android-25", -// 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") + 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, -platformTarget := "android-25" + proguardVersion := "5.3.2", + proguardCache := Seq(), -compileOrder := CompileOrder.JavaThenScala -javacOptions ++= "-source" :: "1.7" :: "-target" :: "1.7" :: Nil -scalacOptions ++= "-target:jvm-1.7" :: "-Xexperimental" :: Nil + shrinkResources := true, + typedResources := false +) -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 +val supportLibsVersion = "25.1.1" +lazy val root = Project(id = "shadowsocks-android", base = file(".")) + .settings(commonSettings) + .aggregate(plugin, mobile) -shrinkResources := true -typedResources := false -resConfigs := Seq("ja", "ru", "zh-rCN", "zh-rTW") +install in Android := (install in (mobile, Android)).value +run in Android := (run in (mobile, Android)).evaluated -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 plugin = project + .settings(commonSettings) + .settings( + libraryDependencies += "com.android.support" % "preference-v14" % supportLibsVersion + ) -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 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) diff --git a/build.sh b/build.sh deleted file mode 100755 index 5a646eab..00000000 --- a/build.sh +++ /dev/null @@ -1,16 +0,0 @@ -#!/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 diff --git a/kcptun/go b/kcptun/go deleted file mode 160000 index c3f75e65..00000000 --- a/kcptun/go +++ /dev/null @@ -1 +0,0 @@ -Subproject commit c3f75e654ddb95bd53351294337649c08e9f9409 diff --git a/kcptun/kcptun b/kcptun/kcptun deleted file mode 160000 index c532a50e..00000000 --- a/kcptun/kcptun +++ /dev/null @@ -1 +0,0 @@ -Subproject commit c532a50e1aeeaa9dee2428b374c3651a5c95258a diff --git a/kcptun/make.bash b/kcptun/make.bash deleted file mode 100755 index 2eb1454d..00000000 --- a/kcptun/make.bash +++ /dev/null @@ -1,69 +0,0 @@ -#!/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" diff --git a/mobile/build.sbt b/mobile/build.sbt new file mode 100644 index 00000000..c20f1f1a --- /dev/null +++ b/mobile/build.sbt @@ -0,0 +1,38 @@ +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 diff --git a/gfwlist/gen.pl b/mobile/gfwlist/gen.pl similarity index 100% rename from gfwlist/gen.pl rename to mobile/gfwlist/gen.pl diff --git a/gfwlist/gen.py b/mobile/gfwlist/gen.py similarity index 100% rename from gfwlist/gen.py rename to mobile/gfwlist/gen.py diff --git a/lint.xml b/mobile/lint.xml similarity index 100% rename from lint.xml rename to mobile/lint.xml diff --git a/local.properties.example b/mobile/local.properties.example similarity index 100% rename from local.properties.example rename to mobile/local.properties.example diff --git a/src/main/AndroidManifest.xml b/mobile/src/main/AndroidManifest.xml similarity index 96% rename from src/main/AndroidManifest.xml rename to mobile/src/main/AndroidManifest.xml index 5594a06b..0f77f4df 100644 --- a/src/main/AndroidManifest.xml +++ b/mobile/src/main/AndroidManifest.xml @@ -2,7 +2,6 @@ xmlns:tools="http://schemas.android.com/tools" package="com.github.shadowsocks"> - @@ -18,14 +17,10 @@ android:targetSdkVersion="25"/> http
  • shadowsocks-libev: GPLv3
  • pdnsd: GPLv3
  • libev: GPLv2
  • -
  • kcptun: MIT
  • libsodium: ISC
  • +
  • libudns: LGPL
  • diff --git a/src/main/ic_launcher-web.png b/mobile/src/main/ic_launcher-web.png similarity index 100% rename from src/main/ic_launcher-web.png rename to mobile/src/main/ic_launcher-web.png diff --git a/src/main/java/com/github/shadowsocks/DialogFragment.java b/mobile/src/main/java/com/github/shadowsocks/DialogFragment.java similarity index 100% rename from src/main/java/com/github/shadowsocks/DialogFragment.java rename to mobile/src/main/java/com/github/shadowsocks/DialogFragment.java diff --git a/src/main/java/com/github/shadowsocks/System.java b/mobile/src/main/java/com/github/shadowsocks/JniHelper.java similarity index 70% rename from src/main/java/com/github/shadowsocks/System.java rename to mobile/src/main/java/com/github/shadowsocks/JniHelper.java index ab167c27..dfce9080 100755 --- a/src/main/java/com/github/shadowsocks/System.java +++ b/mobile/src/main/java/com/github/shadowsocks/JniHelper.java @@ -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); } diff --git a/mobile/src/main/java/com/github/shadowsocks/utils/Commandline.java b/mobile/src/main/java/com/github/shadowsocks/utils/Commandline.java new file mode 100644 index 00000000..e364c567 --- /dev/null +++ b/mobile/src/main/java/com/github/shadowsocks/utils/Commandline.java @@ -0,0 +1,155 @@ +package com.github.shadowsocks.utils; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.StringTokenizer; + +/** + * Commandline objects help handling command lines specifying processes to + * execute. + * + * The class can be used to define a command line as nested elements or as a + * helper to define a command line by an application. + *

    + * + * <someelement>
    + *   <acommandline executable="/executable/to/run">
    + *     <argument value="argument 1" />
    + *     <argument line="argument_1 argument_2 argument_3" />
    + *     <argument value="argument 4" />
    + *   </acommandline>
    + * </someelement>
    + *
    + * + * 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 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 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()]); + } +} diff --git a/src/main/jni/Android.mk b/mobile/src/main/jni/Android.mk similarity index 62% rename from src/main/jni/Android.mk rename to mobile/src/main/jni/Android.mk index ac13a962..8f771c47 100755 --- a/src/main/jni/Android.mk +++ b/mobile/src/main/jni/Android.mk @@ -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 diff --git a/src/main/jni/Application.mk b/mobile/src/main/jni/Application.mk similarity index 77% rename from src/main/jni/Application.mk rename to mobile/src/main/jni/Application.mk index 8ea8eb37..e32dd558 100644 --- a/src/main/jni/Application.mk +++ b/mobile/src/main/jni/Application.mk @@ -1,4 +1,4 @@ APP_ABI := armeabi-v7a x86 -APP_PLATFORM := android-16 +APP_PLATFORM := android-19 APP_STL := stlport_static NDK_TOOLCHAIN_VERSION := clang diff --git a/src/main/jni/badvpn b/mobile/src/main/jni/badvpn similarity index 100% rename from src/main/jni/badvpn rename to mobile/src/main/jni/badvpn diff --git a/src/main/jni/build-shared-executable.mk b/mobile/src/main/jni/build-shared-executable.mk similarity index 100% rename from src/main/jni/build-shared-executable.mk rename to mobile/src/main/jni/build-shared-executable.mk diff --git a/src/main/jni/include/libev/config.h b/mobile/src/main/jni/include/libev/config.h similarity index 100% rename from src/main/jni/include/libev/config.h rename to mobile/src/main/jni/include/libev/config.h diff --git a/src/main/jni/include/pdnsd/config.h b/mobile/src/main/jni/include/pdnsd/config.h similarity index 100% rename from src/main/jni/include/pdnsd/config.h rename to mobile/src/main/jni/include/pdnsd/config.h diff --git a/src/main/jni/include/shadowsocks-libev/config.h b/mobile/src/main/jni/include/shadowsocks-libev/config.h similarity index 100% rename from src/main/jni/include/shadowsocks-libev/config.h rename to mobile/src/main/jni/include/shadowsocks-libev/config.h diff --git a/src/main/jni/include/sodium/version.h b/mobile/src/main/jni/include/sodium/version.h similarity index 100% rename from src/main/jni/include/sodium/version.h rename to mobile/src/main/jni/include/sodium/version.h diff --git a/src/main/jni/system.cpp b/mobile/src/main/jni/jni-helper.cpp similarity index 55% rename from src/main/jni/system.cpp rename to mobile/src/main/jni/jni-helper.cpp index c5e2bc8c..cbba6dbc 100644 --- a/src/main/jni/system.cpp +++ b/mobile/src/main/jni/jni-helper.cpp @@ -1,11 +1,14 @@ -#define LOG_TAG "Shadowsocks" +#define LOG_TAG "JniHelper" #include "jni.h" #include +#include + #include #include #include #include +#include #include #include @@ -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; - - /* Fork off the parent process */ - pid = fork(); - if (pid < 0) { - env->ReleaseStringUTFChars(cmd, cmd_str); - 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; +static int sdk_version() { + char version[PROP_VALUE_MAX + 1]; + __system_property_get("ro.build.version.sdk", version); + return atoi(version); } -void Java_com_github_shadowsocks_system_jniclose(JNIEnv *env, jobject thiz, jint fd) { +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; + } + 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_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; diff --git a/src/main/jni/libancillary b/mobile/src/main/jni/libancillary similarity index 100% rename from src/main/jni/libancillary rename to mobile/src/main/jni/libancillary diff --git a/mobile/src/main/jni/libev b/mobile/src/main/jni/libev new file mode 160000 index 00000000..981fb847 --- /dev/null +++ b/mobile/src/main/jni/libev @@ -0,0 +1 @@ +Subproject commit 981fb847e1e306bc40460de4a810f63f9bbc7212 diff --git a/src/main/jni/libevent b/mobile/src/main/jni/libevent similarity index 100% rename from src/main/jni/libevent rename to mobile/src/main/jni/libevent diff --git a/mobile/src/main/jni/libsodium b/mobile/src/main/jni/libsodium new file mode 160000 index 00000000..e160de24 --- /dev/null +++ b/mobile/src/main/jni/libsodium @@ -0,0 +1 @@ +Subproject commit e160de24fd146ab84b4eb397c7cffbf34775aa02 diff --git a/mobile/src/main/jni/libudns b/mobile/src/main/jni/libudns new file mode 160000 index 00000000..53a1abd2 --- /dev/null +++ b/mobile/src/main/jni/libudns @@ -0,0 +1 @@ +Subproject commit 53a1abd2b6fd7e3bf99ce6e2e267c636618d57ef diff --git a/src/main/jni/mbedtls b/mobile/src/main/jni/mbedtls similarity index 100% rename from src/main/jni/mbedtls rename to mobile/src/main/jni/mbedtls diff --git a/src/main/jni/pcre b/mobile/src/main/jni/pcre similarity index 100% rename from src/main/jni/pcre rename to mobile/src/main/jni/pcre diff --git a/src/main/jni/pdnsd b/mobile/src/main/jni/pdnsd similarity index 100% rename from src/main/jni/pdnsd rename to mobile/src/main/jni/pdnsd diff --git a/src/main/jni/redsocks b/mobile/src/main/jni/redsocks similarity index 100% rename from src/main/jni/redsocks rename to mobile/src/main/jni/redsocks diff --git a/mobile/src/main/jni/shadowsocks-libev b/mobile/src/main/jni/shadowsocks-libev new file mode 160000 index 00000000..f4b16416 --- /dev/null +++ b/mobile/src/main/jni/shadowsocks-libev @@ -0,0 +1 @@ +Subproject commit f4b1641636942bc44c96160afad9f023fb6dbf0e diff --git a/src/main/res/drawable-anydpi-v21/ic_navigation_close.xml b/mobile/src/main/res/drawable-anydpi-v21/ic_navigation_close.xml similarity index 100% rename from src/main/res/drawable-anydpi-v21/ic_navigation_close.xml rename to mobile/src/main/res/drawable-anydpi-v21/ic_navigation_close.xml diff --git a/src/main/res/drawable-hdpi/ic_navigation_close.png b/mobile/src/main/res/drawable-hdpi/ic_navigation_close.png similarity index 100% rename from src/main/res/drawable-hdpi/ic_navigation_close.png rename to mobile/src/main/res/drawable-hdpi/ic_navigation_close.png diff --git a/src/main/res/drawable-hdpi/ic_stat_shadowsocks.png b/mobile/src/main/res/drawable-hdpi/ic_stat_shadowsocks.png similarity index 100% rename from src/main/res/drawable-hdpi/ic_stat_shadowsocks.png rename to mobile/src/main/res/drawable-hdpi/ic_stat_shadowsocks.png diff --git a/src/main/res/drawable-mdpi/ic_navigation_close.png b/mobile/src/main/res/drawable-mdpi/ic_navigation_close.png similarity index 100% rename from src/main/res/drawable-mdpi/ic_navigation_close.png rename to mobile/src/main/res/drawable-mdpi/ic_navigation_close.png diff --git a/src/main/res/drawable-mdpi/ic_stat_shadowsocks.png b/mobile/src/main/res/drawable-mdpi/ic_stat_shadowsocks.png similarity index 100% rename from src/main/res/drawable-mdpi/ic_stat_shadowsocks.png rename to mobile/src/main/res/drawable-mdpi/ic_stat_shadowsocks.png diff --git a/src/main/res/drawable-v21/background_selectable.xml b/mobile/src/main/res/drawable-v21/background_selectable.xml similarity index 100% rename from src/main/res/drawable-v21/background_selectable.xml rename to mobile/src/main/res/drawable-v21/background_selectable.xml diff --git a/src/main/res/drawable-v21/background_stat.xml b/mobile/src/main/res/drawable-v21/background_stat.xml similarity index 100% rename from src/main/res/drawable-v21/background_stat.xml rename to mobile/src/main/res/drawable-v21/background_stat.xml diff --git a/src/main/res/drawable-xhdpi/ic_navigation_close.png b/mobile/src/main/res/drawable-xhdpi/ic_navigation_close.png similarity index 100% rename from src/main/res/drawable-xhdpi/ic_navigation_close.png rename to mobile/src/main/res/drawable-xhdpi/ic_navigation_close.png diff --git a/src/main/res/drawable-xhdpi/ic_stat_shadowsocks.png b/mobile/src/main/res/drawable-xhdpi/ic_stat_shadowsocks.png similarity index 100% rename from src/main/res/drawable-xhdpi/ic_stat_shadowsocks.png rename to mobile/src/main/res/drawable-xhdpi/ic_stat_shadowsocks.png diff --git a/src/main/res/drawable-xxhdpi/ic_navigation_close.png b/mobile/src/main/res/drawable-xxhdpi/ic_navigation_close.png similarity index 100% rename from src/main/res/drawable-xxhdpi/ic_navigation_close.png rename to mobile/src/main/res/drawable-xxhdpi/ic_navigation_close.png diff --git a/src/main/res/drawable-xxhdpi/ic_stat_shadowsocks.png b/mobile/src/main/res/drawable-xxhdpi/ic_stat_shadowsocks.png similarity index 100% rename from src/main/res/drawable-xxhdpi/ic_stat_shadowsocks.png rename to mobile/src/main/res/drawable-xxhdpi/ic_stat_shadowsocks.png diff --git a/src/main/res/drawable-xxxhdpi/ic_navigation_close.png b/mobile/src/main/res/drawable-xxxhdpi/ic_navigation_close.png similarity index 100% rename from src/main/res/drawable-xxxhdpi/ic_navigation_close.png rename to mobile/src/main/res/drawable-xxxhdpi/ic_navigation_close.png diff --git a/src/main/res/drawable/background_header.png b/mobile/src/main/res/drawable/background_header.png similarity index 100% rename from src/main/res/drawable/background_header.png rename to mobile/src/main/res/drawable/background_header.png diff --git a/src/main/res/drawable/background_profile.xml b/mobile/src/main/res/drawable/background_profile.xml similarity index 100% rename from src/main/res/drawable/background_profile.xml rename to mobile/src/main/res/drawable/background_profile.xml diff --git a/src/main/res/drawable/background_selectable.xml b/mobile/src/main/res/drawable/background_selectable.xml similarity index 100% rename from src/main/res/drawable/background_selectable.xml rename to mobile/src/main/res/drawable/background_selectable.xml diff --git a/src/main/res/drawable/background_stat.xml b/mobile/src/main/res/drawable/background_stat.xml similarity index 100% rename from src/main/res/drawable/background_stat.xml rename to mobile/src/main/res/drawable/background_stat.xml diff --git a/src/main/res/drawable/ic_action_assignment.xml b/mobile/src/main/res/drawable/ic_action_assignment.xml similarity index 100% rename from src/main/res/drawable/ic_action_assignment.xml rename to mobile/src/main/res/drawable/ic_action_assignment.xml diff --git a/src/main/res/drawable/ic_action_copyright.xml b/mobile/src/main/res/drawable/ic_action_copyright.xml similarity index 100% rename from src/main/res/drawable/ic_action_copyright.xml rename to mobile/src/main/res/drawable/ic_action_copyright.xml diff --git a/src/main/res/drawable/ic_action_delete.xml b/mobile/src/main/res/drawable/ic_action_delete.xml similarity index 100% rename from src/main/res/drawable/ic_action_delete.xml rename to mobile/src/main/res/drawable/ic_action_delete.xml diff --git a/src/main/res/drawable/ic_action_description.xml b/mobile/src/main/res/drawable/ic_action_description.xml similarity index 100% rename from src/main/res/drawable/ic_action_description.xml rename to mobile/src/main/res/drawable/ic_action_description.xml diff --git a/src/main/res/drawable/ic_action_done.xml b/mobile/src/main/res/drawable/ic_action_done.xml similarity index 100% rename from src/main/res/drawable/ic_action_done.xml rename to mobile/src/main/res/drawable/ic_action_done.xml diff --git a/src/main/res/drawable/ic_action_help_outline.xml b/mobile/src/main/res/drawable/ic_action_help_outline.xml similarity index 100% rename from src/main/res/drawable/ic_action_help_outline.xml rename to mobile/src/main/res/drawable/ic_action_help_outline.xml diff --git a/src/main/res/drawable/ic_action_note_add.xml b/mobile/src/main/res/drawable/ic_action_note_add.xml similarity index 100% rename from src/main/res/drawable/ic_action_note_add.xml rename to mobile/src/main/res/drawable/ic_action_note_add.xml diff --git a/src/main/res/drawable/ic_action_settings.xml b/mobile/src/main/res/drawable/ic_action_settings.xml similarity index 100% rename from src/main/res/drawable/ic_action_settings.xml rename to mobile/src/main/res/drawable/ic_action_settings.xml diff --git a/src/main/res/drawable/ic_av_playlist_add.xml b/mobile/src/main/res/drawable/ic_av_playlist_add.xml similarity index 100% rename from src/main/res/drawable/ic_av_playlist_add.xml rename to mobile/src/main/res/drawable/ic_av_playlist_add.xml diff --git a/src/main/res/drawable/ic_content_copy.xml b/mobile/src/main/res/drawable/ic_content_copy.xml similarity index 100% rename from src/main/res/drawable/ic_content_copy.xml rename to mobile/src/main/res/drawable/ic_content_copy.xml diff --git a/src/main/res/drawable/ic_content_paste.xml b/mobile/src/main/res/drawable/ic_content_paste.xml similarity index 100% rename from src/main/res/drawable/ic_content_paste.xml rename to mobile/src/main/res/drawable/ic_content_paste.xml diff --git a/src/main/res/drawable/ic_image_edit.xml b/mobile/src/main/res/drawable/ic_image_edit.xml similarity index 100% rename from src/main/res/drawable/ic_image_edit.xml rename to mobile/src/main/res/drawable/ic_image_edit.xml diff --git a/src/main/res/drawable/ic_navigation_refresh.xml b/mobile/src/main/res/drawable/ic_navigation_refresh.xml similarity index 100% rename from src/main/res/drawable/ic_navigation_refresh.xml rename to mobile/src/main/res/drawable/ic_navigation_refresh.xml diff --git a/src/main/res/drawable/ic_qu_shadowsocks_launcher.xml b/mobile/src/main/res/drawable/ic_qu_shadowsocks_launcher.xml similarity index 100% rename from src/main/res/drawable/ic_qu_shadowsocks_launcher.xml rename to mobile/src/main/res/drawable/ic_qu_shadowsocks_launcher.xml diff --git a/src/main/res/drawable/ic_social_share.xml b/mobile/src/main/res/drawable/ic_social_share.xml similarity index 100% rename from src/main/res/drawable/ic_social_share.xml rename to mobile/src/main/res/drawable/ic_social_share.xml diff --git a/src/main/res/drawable/ic_start_busy.xml b/mobile/src/main/res/drawable/ic_start_busy.xml similarity index 100% rename from src/main/res/drawable/ic_start_busy.xml rename to mobile/src/main/res/drawable/ic_start_busy.xml diff --git a/src/main/res/drawable/ic_start_connected.xml b/mobile/src/main/res/drawable/ic_start_connected.xml similarity index 100% rename from src/main/res/drawable/ic_start_connected.xml rename to mobile/src/main/res/drawable/ic_start_connected.xml diff --git a/src/main/res/drawable/ic_start_idle.xml b/mobile/src/main/res/drawable/ic_start_idle.xml similarity index 100% rename from src/main/res/drawable/ic_start_idle.xml rename to mobile/src/main/res/drawable/ic_start_idle.xml diff --git a/src/main/res/layout/dialog_acl_rule.xml b/mobile/src/main/res/layout/dialog_acl_rule.xml similarity index 87% rename from src/main/res/layout/dialog_acl_rule.xml rename to mobile/src/main/res/layout/dialog_acl_rule.xml index 6750be86..dfb27e8d 100644 --- a/src/main/res/layout/dialog_acl_rule.xml +++ b/mobile/src/main/res/layout/dialog_acl_rule.xml @@ -10,6 +10,5 @@ + android:inputType="textNoSuggestions|textMultiLine"/> diff --git a/mobile/src/main/res/layout/icon_list_item_2.xml b/mobile/src/main/res/layout/icon_list_item_2.xml new file mode 100644 index 00000000..4b49fa6c --- /dev/null +++ b/mobile/src/main/res/layout/icon_list_item_2.xml @@ -0,0 +1,32 @@ + + + + + + + + + diff --git a/src/main/res/layout/layout_about.xml b/mobile/src/main/res/layout/layout_about.xml similarity index 100% rename from src/main/res/layout/layout_about.xml rename to mobile/src/main/res/layout/layout_about.xml diff --git a/src/main/res/layout/layout_apps.xml b/mobile/src/main/res/layout/layout_apps.xml similarity index 100% rename from src/main/res/layout/layout_apps.xml rename to mobile/src/main/res/layout/layout_apps.xml diff --git a/src/main/res/layout/layout_apps_item.xml b/mobile/src/main/res/layout/layout_apps_item.xml similarity index 100% rename from src/main/res/layout/layout_apps_item.xml rename to mobile/src/main/res/layout/layout_apps_item.xml diff --git a/src/main/res/layout/layout_custom_rules.xml b/mobile/src/main/res/layout/layout_custom_rules.xml similarity index 100% rename from src/main/res/layout/layout_custom_rules.xml rename to mobile/src/main/res/layout/layout_custom_rules.xml diff --git a/src/main/res/layout/layout_global_settings.xml b/mobile/src/main/res/layout/layout_global_settings.xml similarity index 100% rename from src/main/res/layout/layout_global_settings.xml rename to mobile/src/main/res/layout/layout_global_settings.xml diff --git a/src/main/res/layout/layout_header.xml b/mobile/src/main/res/layout/layout_header.xml similarity index 100% rename from src/main/res/layout/layout_header.xml rename to mobile/src/main/res/layout/layout_header.xml diff --git a/src/main/res/layout/layout_list.xml b/mobile/src/main/res/layout/layout_list.xml similarity index 100% rename from src/main/res/layout/layout_list.xml rename to mobile/src/main/res/layout/layout_list.xml diff --git a/src/main/res/layout/layout_main.xml b/mobile/src/main/res/layout/layout_main.xml similarity index 100% rename from src/main/res/layout/layout_main.xml rename to mobile/src/main/res/layout/layout_main.xml diff --git a/src/main/res/layout/layout_profile.xml b/mobile/src/main/res/layout/layout_profile.xml similarity index 83% rename from src/main/res/layout/layout_profile.xml rename to mobile/src/main/res/layout/layout_profile.xml index 35cd1447..aacb3bd8 100644 --- a/src/main/res/layout/layout_profile.xml +++ b/mobile/src/main/res/layout/layout_profile.xml @@ -24,7 +24,7 @@ android:layout_height="wrap_content" android:layout_marginStart="8dp" android:id="@+id/container"> - - - + android:layout_marginTop="4dp"> + - + diff --git a/src/main/res/layout/layout_profile_config.xml b/mobile/src/main/res/layout/layout_profile_config.xml similarity index 100% rename from src/main/res/layout/layout_profile_config.xml rename to mobile/src/main/res/layout/layout_profile_config.xml diff --git a/src/main/res/layout/layout_tasker.xml b/mobile/src/main/res/layout/layout_tasker.xml similarity index 100% rename from src/main/res/layout/layout_tasker.xml rename to mobile/src/main/res/layout/layout_tasker.xml diff --git a/src/main/res/menu/app_manager_menu.xml b/mobile/src/main/res/menu/app_manager_menu.xml similarity index 100% rename from src/main/res/menu/app_manager_menu.xml rename to mobile/src/main/res/menu/app_manager_menu.xml diff --git a/src/main/res/menu/custom_rules_menu.xml b/mobile/src/main/res/menu/custom_rules_menu.xml similarity index 100% rename from src/main/res/menu/custom_rules_menu.xml rename to mobile/src/main/res/menu/custom_rules_menu.xml diff --git a/src/main/res/menu/profile_config_menu.xml b/mobile/src/main/res/menu/profile_config_menu.xml similarity index 100% rename from src/main/res/menu/profile_config_menu.xml rename to mobile/src/main/res/menu/profile_config_menu.xml diff --git a/src/main/res/menu/profile_manager_menu.xml b/mobile/src/main/res/menu/profile_manager_menu.xml similarity index 100% rename from src/main/res/menu/profile_manager_menu.xml rename to mobile/src/main/res/menu/profile_manager_menu.xml diff --git a/src/main/res/menu/profile_share_popup.xml b/mobile/src/main/res/menu/profile_share_popup.xml similarity index 100% rename from src/main/res/menu/profile_share_popup.xml rename to mobile/src/main/res/menu/profile_share_popup.xml diff --git a/src/main/res/mipmap-hdpi/ic_launcher.png b/mobile/src/main/res/mipmap-hdpi/ic_launcher.png similarity index 100% rename from src/main/res/mipmap-hdpi/ic_launcher.png rename to mobile/src/main/res/mipmap-hdpi/ic_launcher.png diff --git a/src/main/res/mipmap-mdpi/ic_launcher.png b/mobile/src/main/res/mipmap-mdpi/ic_launcher.png similarity index 100% rename from src/main/res/mipmap-mdpi/ic_launcher.png rename to mobile/src/main/res/mipmap-mdpi/ic_launcher.png diff --git a/src/main/res/mipmap-xhdpi/ic_launcher.png b/mobile/src/main/res/mipmap-xhdpi/ic_launcher.png similarity index 100% rename from src/main/res/mipmap-xhdpi/ic_launcher.png rename to mobile/src/main/res/mipmap-xhdpi/ic_launcher.png diff --git a/src/main/res/mipmap-xxhdpi/ic_launcher.png b/mobile/src/main/res/mipmap-xxhdpi/ic_launcher.png similarity index 100% rename from src/main/res/mipmap-xxhdpi/ic_launcher.png rename to mobile/src/main/res/mipmap-xxhdpi/ic_launcher.png diff --git a/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/mobile/src/main/res/mipmap-xxxhdpi/ic_launcher.png similarity index 100% rename from src/main/res/mipmap-xxxhdpi/ic_launcher.png rename to mobile/src/main/res/mipmap-xxxhdpi/ic_launcher.png diff --git a/src/main/res/raw/gtm_default_container b/mobile/src/main/res/raw/gtm_default_container similarity index 100% rename from src/main/res/raw/gtm_default_container rename to mobile/src/main/res/raw/gtm_default_container diff --git a/src/main/res/values-ja/strings.xml b/mobile/src/main/res/values-ja/strings.xml similarity index 100% rename from src/main/res/values-ja/strings.xml rename to mobile/src/main/res/values-ja/strings.xml diff --git a/src/main/res/values-ru/strings.xml b/mobile/src/main/res/values-ru/strings.xml similarity index 100% rename from src/main/res/values-ru/strings.xml rename to mobile/src/main/res/values-ru/strings.xml diff --git a/src/main/res/values-v21/dimen.xml b/mobile/src/main/res/values-v21/dimen.xml similarity index 100% rename from src/main/res/values-v21/dimen.xml rename to mobile/src/main/res/values-v21/dimen.xml diff --git a/src/main/res/values-v21/strings.xml b/mobile/src/main/res/values-v21/strings.xml similarity index 100% rename from src/main/res/values-v21/strings.xml rename to mobile/src/main/res/values-v21/strings.xml diff --git a/src/main/res/values-zh-rCN/strings.xml b/mobile/src/main/res/values-zh-rCN/strings.xml similarity index 100% rename from src/main/res/values-zh-rCN/strings.xml rename to mobile/src/main/res/values-zh-rCN/strings.xml diff --git a/src/main/res/values-zh-rTW/strings.xml b/mobile/src/main/res/values-zh-rTW/strings.xml similarity index 100% rename from src/main/res/values-zh-rTW/strings.xml rename to mobile/src/main/res/values-zh-rTW/strings.xml diff --git a/src/main/res/values/arrays.xml b/mobile/src/main/res/values/arrays.xml similarity index 96% rename from src/main/res/values/arrays.xml rename to mobile/src/main/res/values/arrays.xml index 58e8ae88..8e7b2944 100644 --- a/src/main/res/values/arrays.xml +++ b/mobile/src/main/res/values/arrays.xml @@ -1,8 +1,6 @@ - TABLE - RC4 RC4-MD5 AES-128-CFB AES-192-CFB @@ -17,11 +15,13 @@ SALSA20 CHACHA20 CHACHA20-IETF + AES-128-GCM + AES-192-GCM + AES-256-GCM + CHACHA20-IETF-POLY1305 - table - rc4 rc4-md5 aes-128-cfb aes-192-cfb @@ -36,6 +36,10 @@ salsa20 chacha20 chacha20-ietf + aes-128-gcm + aes-192-gcm + aes-256-gcm + chacha20-ietf-poly1305 diff --git a/src/main/res/values/attrs.xml b/mobile/src/main/res/values/attrs.xml similarity index 59% rename from src/main/res/values/attrs.xml rename to mobile/src/main/res/values/attrs.xml index 0671660e..755593c8 100644 --- a/src/main/res/values/attrs.xml +++ b/mobile/src/main/res/values/attrs.xml @@ -4,8 +4,4 @@ - - - - diff --git a/src/main/res/values/configs.xml b/mobile/src/main/res/values/configs.xml similarity index 100% rename from src/main/res/values/configs.xml rename to mobile/src/main/res/values/configs.xml diff --git a/src/main/res/values/dimen.xml b/mobile/src/main/res/values/dimen.xml similarity index 76% rename from src/main/res/values/dimen.xml rename to mobile/src/main/res/values/dimen.xml index ad252519..415496c2 100644 --- a/src/main/res/values/dimen.xml +++ b/mobile/src/main/res/values/dimen.xml @@ -1,6 +1,8 @@ 480dp + 250dp + 8dp -44dp diff --git a/src/main/res/values/strings.xml b/mobile/src/main/res/values/strings.xml similarity index 89% rename from src/main/res/values/strings.xml rename to mobile/src/main/res/values/strings.xml index b14816a5..17d2f7fe 100644 --- a/src/main/res/values/strings.xml +++ b/mobile/src/main/res/values/strings.xml @@ -26,7 +26,6 @@ Error code: #%d - Server Settings Profile Name Server Remote Port @@ -35,11 +34,8 @@ Encrypt Method - Feature Settings IPv6 Route Redirect IPv6 traffic to remote - One-time Authentication - Enable one-time authentication Route All Bypass LAN @@ -64,7 +60,6 @@ Shadowsocks started. Invalid server name Failed to connect the remote server - WARNING: The current encryption method is unsafe WARNING: NAT mode has been deprecated since Android 5.0 NAT mode requires ROOT permission Switch to VPN mode @@ -75,8 +70,6 @@ No valid profile data found. - Yes - No Close Please select a profile Proxy/Password should not be empty @@ -105,8 +98,6 @@ Profile config - Changes not saved. Do you want to save? - Apply Remove Are you sure you want to remove this profile? QR code/NFC @@ -135,12 +126,6 @@ Connected Not connected - - Enable Shadowsocks over KCP - KCP Port - %d (port number of the remote KCPTUN server) - KCP Parameters (CLI) - Custom rules Selection… @@ -148,4 +133,12 @@ Subnet/Hostname PCRE pattern Domain name and all its subdomain names Edit rule + + + Plugin + Configure… + Disabled + Unknown plugin %s + Warning: This plugin does not seem to come from a known trusted source. + Plugin: %s diff --git a/mobile/src/main/res/values/styles.xml b/mobile/src/main/res/values/styles.xml new file mode 100644 index 00000000..956d9f2d --- /dev/null +++ b/mobile/src/main/res/values/styles.xml @@ -0,0 +1,11 @@ + + + + + diff --git a/src/main/res/xml/pref_global.xml b/mobile/src/main/res/xml/pref_global.xml similarity index 100% rename from src/main/res/xml/pref_global.xml rename to mobile/src/main/res/xml/pref_global.xml diff --git a/src/main/res/xml/pref_profile.xml b/mobile/src/main/res/xml/pref_profile.xml similarity index 76% rename from src/main/res/xml/pref_profile.xml rename to mobile/src/main/res/xml/pref_profile.xml index 80816c63..37d88686 100644 --- a/src/main/res/xml/pref_profile.xml +++ b/mobile/src/main/res/xml/pref_profile.xml @@ -2,13 +2,14 @@ + + - - @@ -72,24 +69,18 @@ + android:title="@string/plugin"> - - - - - + android:title="@string/plugin"/> + diff --git a/src/main/res/xml/shortcuts.xml b/mobile/src/main/res/xml/shortcuts.xml similarity index 100% rename from src/main/res/xml/shortcuts.xml rename to mobile/src/main/res/xml/shortcuts.xml diff --git a/src/main/res/xml/tracker.xml b/mobile/src/main/res/xml/tracker.xml similarity index 100% rename from src/main/res/xml/tracker.xml rename to mobile/src/main/res/xml/tracker.xml diff --git a/src/main/scala/com/github/shadowsocks/AboutFragment.scala b/mobile/src/main/scala/com/github/shadowsocks/AboutFragment.scala similarity index 100% rename from src/main/scala/com/github/shadowsocks/AboutFragment.scala rename to mobile/src/main/scala/com/github/shadowsocks/AboutFragment.scala diff --git a/src/main/scala/com/github/shadowsocks/AppManager.scala b/mobile/src/main/scala/com/github/shadowsocks/AppManager.scala similarity index 94% rename from src/main/scala/com/github/shadowsocks/AppManager.scala rename to mobile/src/main/scala/com/github/shadowsocks/AppManager.scala index b61c2705..ceffb9d3 100644 --- a/src/main/scala/com/github/shadowsocks/AppManager.scala +++ b/mobile/src/main/scala/com/github/shadowsocks/AppManager.scala @@ -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) diff --git a/src/main/scala/com/github/shadowsocks/BaseService.scala b/mobile/src/main/scala/com/github/shadowsocks/BaseService.scala similarity index 88% rename from src/main/scala/com/github/shadowsocks/BaseService.scala rename to mobile/src/main/scala/com/github/shadowsocks/BaseService.scala index 3b532114..5d5ec16d 100644 --- a/src/main/scala/com/github/shadowsocks/BaseService.scala +++ b/mobile/src/main/scala/com/github/shadowsocks/BaseService.scala @@ -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 + } } diff --git a/src/main/scala/com/github/shadowsocks/BootReceiver.scala b/mobile/src/main/scala/com/github/shadowsocks/BootReceiver.scala similarity index 100% rename from src/main/scala/com/github/shadowsocks/BootReceiver.scala rename to mobile/src/main/scala/com/github/shadowsocks/BootReceiver.scala diff --git a/src/main/scala/com/github/shadowsocks/GlobalConfigFragment.scala b/mobile/src/main/scala/com/github/shadowsocks/GlobalConfigFragment.scala similarity index 82% rename from src/main/scala/com/github/shadowsocks/GlobalConfigFragment.scala rename to mobile/src/main/scala/com/github/shadowsocks/GlobalConfigFragment.scala index c6470bf2..a3de92be 100644 --- a/src/main/scala/com/github/shadowsocks/GlobalConfigFragment.scala +++ b/mobile/src/main/scala/com/github/shadowsocks/GlobalConfigFragment.scala @@ -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 _ => } } diff --git a/src/main/scala/com/github/shadowsocks/GlobalSettingsFragment.scala b/mobile/src/main/scala/com/github/shadowsocks/GlobalSettingsFragment.scala similarity index 100% rename from src/main/scala/com/github/shadowsocks/GlobalSettingsFragment.scala rename to mobile/src/main/scala/com/github/shadowsocks/GlobalSettingsFragment.scala diff --git a/src/main/scala/com/github/shadowsocks/GuardedProcess.scala b/mobile/src/main/scala/com/github/shadowsocks/GuardedProcess.scala similarity index 83% rename from src/main/scala/com/github/shadowsocks/GuardedProcess.scala rename to mobile/src/main/scala/com/github/shadowsocks/GuardedProcess.scala index bdbf77b6..7ff41694 100644 --- a/src/main/scala/com/github/shadowsocks/GuardedProcess.scala +++ b/mobile/src/main/scala/com/github/shadowsocks/GuardedProcess.scala @@ -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() } } diff --git a/src/main/scala/com/github/shadowsocks/MainActivity.scala b/mobile/src/main/scala/com/github/shadowsocks/MainActivity.scala similarity index 93% rename from src/main/scala/com/github/shadowsocks/MainActivity.scala rename to mobile/src/main/scala/com/github/shadowsocks/MainActivity.scala index 7bab1a25..01ca9687 100644 --- a/src/main/scala/com/github/shadowsocks/MainActivity.scala +++ b/mobile/src/main/scala/com/github/shadowsocks/MainActivity.scala @@ -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) diff --git a/src/main/scala/com/github/shadowsocks/ProfileConfigActivity.scala b/mobile/src/main/scala/com/github/shadowsocks/ProfileConfigActivity.scala similarity index 82% rename from src/main/scala/com/github/shadowsocks/ProfileConfigActivity.scala rename to mobile/src/main/scala/com/github/shadowsocks/ProfileConfigActivity.scala index e8344b87..fd22a282 100644 --- a/src/main/scala/com/github/shadowsocks/ProfileConfigActivity.scala +++ b/mobile/src/main/scala/com/github/shadowsocks/ProfileConfigActivity.scala @@ -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) + } } diff --git a/src/main/scala/com/github/shadowsocks/ProfileConfigFragment.scala b/mobile/src/main/scala/com/github/shadowsocks/ProfileConfigFragment.scala similarity index 53% rename from src/main/scala/com/github/shadowsocks/ProfileConfigFragment.scala rename to mobile/src/main/scala/com/github/shadowsocks/ProfileConfigFragment.scala index f558b81f..506f459f 100644 --- a/src/main/scala/com/github/shadowsocks/ProfileConfigFragment.scala +++ b/mobile/src/main/scala/com/github/shadowsocks/ProfileConfigFragment.scala @@ -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 { diff --git a/src/main/scala/com/github/shadowsocks/ProfilesFragment.scala b/mobile/src/main/scala/com/github/shadowsocks/ProfilesFragment.scala similarity index 91% rename from src/main/scala/com/github/shadowsocks/ProfilesFragment.scala rename to mobile/src/main/scala/com/github/shadowsocks/ProfilesFragment.scala index bc5b4fbe..0fd3efa2 100644 --- a/src/main/scala/com/github/shadowsocks/ProfilesFragment.scala +++ b/mobile/src/main/scala/com/github/shadowsocks/ProfilesFragment.scala @@ -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 => diff --git a/src/main/scala/com/github/shadowsocks/QRCodeDialog.scala b/mobile/src/main/scala/com/github/shadowsocks/QRCodeDialog.scala similarity index 96% rename from src/main/scala/com/github/shadowsocks/QRCodeDialog.scala rename to mobile/src/main/scala/com/github/shadowsocks/QRCodeDialog.scala index 255444dc..66317aa8 100644 --- a/src/main/scala/com/github/shadowsocks/QRCodeDialog.scala +++ b/mobile/src/main/scala/com/github/shadowsocks/QRCodeDialog.scala @@ -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 diff --git a/src/main/scala/com/github/shadowsocks/QuickToggleShortcut.scala b/mobile/src/main/scala/com/github/shadowsocks/QuickToggleShortcut.scala similarity index 100% rename from src/main/scala/com/github/shadowsocks/QuickToggleShortcut.scala rename to mobile/src/main/scala/com/github/shadowsocks/QuickToggleShortcut.scala diff --git a/src/main/scala/com/github/shadowsocks/ServiceBoundContext.scala b/mobile/src/main/scala/com/github/shadowsocks/ServiceBoundContext.scala similarity index 100% rename from src/main/scala/com/github/shadowsocks/ServiceBoundContext.scala rename to mobile/src/main/scala/com/github/shadowsocks/ServiceBoundContext.scala diff --git a/src/main/scala/com/github/shadowsocks/ShadowsocksApplication.scala b/mobile/src/main/scala/com/github/shadowsocks/ShadowsocksApplication.scala similarity index 92% rename from src/main/scala/com/github/shadowsocks/ShadowsocksApplication.scala rename to mobile/src/main/scala/com/github/shadowsocks/ShadowsocksApplication.scala index 68ac4dac..5d79a7c6 100644 --- a/src/main/scala/com/github/shadowsocks/ShadowsocksApplication.scala +++ b/mobile/src/main/scala/com/github/shadowsocks/ShadowsocksApplication.scala @@ -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 + } } diff --git a/src/main/scala/com/github/shadowsocks/ShadowsocksBackupAgent.scala b/mobile/src/main/scala/com/github/shadowsocks/ShadowsocksBackupAgent.scala similarity index 98% rename from src/main/scala/com/github/shadowsocks/ShadowsocksBackupAgent.scala rename to mobile/src/main/scala/com/github/shadowsocks/ShadowsocksBackupAgent.scala index 8c26810a..37e8657a 100644 --- a/src/main/scala/com/github/shadowsocks/ShadowsocksBackupAgent.scala +++ b/mobile/src/main/scala/com/github/shadowsocks/ShadowsocksBackupAgent.scala @@ -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")) } } diff --git a/src/main/scala/com/github/shadowsocks/ShadowsocksNatService.scala b/mobile/src/main/scala/com/github/shadowsocks/ShadowsocksNatService.scala similarity index 61% rename from src/main/scala/com/github/shadowsocks/ShadowsocksNatService.scala rename to mobile/src/main/scala/com/github/shadowsocks/ShadowsocksNatService.scala index 58ad4139..f5cb3b36 100644 --- a/src/main/scala/com/github/shadowsocks/ShadowsocksNatService.scala +++ b/mobile/src/main/scala/com/github/shadowsocks/ShadowsocksNatService.scala @@ -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") + 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")) - if (profile.auth) cmd += "-A" + if (profile.udpdns) cmd.append("-u") - if (BuildConfig.DEBUG) Log.d(TAG, cmd.mkString(" ")) - - sstunnelProcess = new GuardedProcess(cmd).start() - - } 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 diff --git a/src/main/scala/com/github/shadowsocks/ShadowsocksNotification.scala b/mobile/src/main/scala/com/github/shadowsocks/ShadowsocksNotification.scala similarity index 100% rename from src/main/scala/com/github/shadowsocks/ShadowsocksNotification.scala rename to mobile/src/main/scala/com/github/shadowsocks/ShadowsocksNotification.scala diff --git a/src/main/scala/com/github/shadowsocks/ShadowsocksRunnerActivity.scala b/mobile/src/main/scala/com/github/shadowsocks/ShadowsocksRunnerActivity.scala similarity index 100% rename from src/main/scala/com/github/shadowsocks/ShadowsocksRunnerActivity.scala rename to mobile/src/main/scala/com/github/shadowsocks/ShadowsocksRunnerActivity.scala diff --git a/src/main/scala/com/github/shadowsocks/ShadowsocksRunnerService.scala b/mobile/src/main/scala/com/github/shadowsocks/ShadowsocksRunnerService.scala similarity index 100% rename from src/main/scala/com/github/shadowsocks/ShadowsocksRunnerService.scala rename to mobile/src/main/scala/com/github/shadowsocks/ShadowsocksRunnerService.scala diff --git a/src/main/scala/com/github/shadowsocks/ShadowsocksTileService.scala b/mobile/src/main/scala/com/github/shadowsocks/ShadowsocksTileService.scala similarity index 100% rename from src/main/scala/com/github/shadowsocks/ShadowsocksTileService.scala rename to mobile/src/main/scala/com/github/shadowsocks/ShadowsocksTileService.scala diff --git a/src/main/scala/com/github/shadowsocks/ShadowsocksVpnService.scala b/mobile/src/main/scala/com/github/shadowsocks/ShadowsocksVpnService.scala similarity index 61% rename from src/main/scala/com/github/shadowsocks/ShadowsocksVpnService.scala rename to mobile/src/main/scala/com/github/shadowsocks/ShadowsocksVpnService.scala index 9218aee5..38219492 100644 --- a/src/main/scala/com/github/shadowsocks/ShadowsocksVpnService.scala +++ b/mobile/src/main/scala/com/github/shadowsocks/ShadowsocksVpnService.scala @@ -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", + "-l", profile.localPort.toString, + "-t", "600", + "-c", buildShadowsocksConfig("ss-local-vpn.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") - - 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") + val cmd = Array(getApplicationInfo.nativeLibraryDir + "/libpdnsd.so", "-c", "pdnsd-vpn.conf") - if (BuildConfig.DEBUG) Log.d(TAG, cmd.mkString(" ")) - - 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 diff --git a/src/main/scala/com/github/shadowsocks/ShadowsocksVpnThread.scala b/mobile/src/main/scala/com/github/shadowsocks/ShadowsocksVpnThread.scala similarity index 91% rename from src/main/scala/com/github/shadowsocks/ShadowsocksVpnThread.scala rename to mobile/src/main/scala/com/github/shadowsocks/ShadowsocksVpnThread.scala index 63d7d25d..42eddfd7 100644 --- a/src/main/scala/com/github/shadowsocks/ShadowsocksVpnThread.scala +++ b/mobile/src/main/scala/com/github/shadowsocks/ShadowsocksVpnThread.scala @@ -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) diff --git a/src/main/scala/com/github/shadowsocks/TaskerActivity.scala b/mobile/src/main/scala/com/github/shadowsocks/TaskerActivity.scala similarity index 100% rename from src/main/scala/com/github/shadowsocks/TaskerActivity.scala rename to mobile/src/main/scala/com/github/shadowsocks/TaskerActivity.scala diff --git a/src/main/scala/com/github/shadowsocks/TaskerReceiver.scala b/mobile/src/main/scala/com/github/shadowsocks/TaskerReceiver.scala similarity index 100% rename from src/main/scala/com/github/shadowsocks/TaskerReceiver.scala rename to mobile/src/main/scala/com/github/shadowsocks/TaskerReceiver.scala diff --git a/src/main/scala/com/github/shadowsocks/ToolbarFragment.scala b/mobile/src/main/scala/com/github/shadowsocks/ToolbarFragment.scala similarity index 98% rename from src/main/scala/com/github/shadowsocks/ToolbarFragment.scala rename to mobile/src/main/scala/com/github/shadowsocks/ToolbarFragment.scala index 3afe18f3..e21a6b80 100644 --- a/src/main/scala/com/github/shadowsocks/ToolbarFragment.scala +++ b/mobile/src/main/scala/com/github/shadowsocks/ToolbarFragment.scala @@ -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 } diff --git a/src/main/scala/com/github/shadowsocks/acl/Acl.scala b/mobile/src/main/scala/com/github/shadowsocks/acl/Acl.scala similarity index 96% rename from src/main/scala/com/github/shadowsocks/acl/Acl.scala rename to mobile/src/main/scala/com/github/shadowsocks/acl/Acl.scala index 715d11cb..676f09e4 100644 --- a/src/main/scala/com/github/shadowsocks/acl/Acl.scala +++ b/mobile/src/main/scala/com/github/shadowsocks/acl/Acl.scala @@ -1,6 +1,6 @@ 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) } diff --git a/src/main/scala/com/github/shadowsocks/acl/AclSyncJob.scala b/mobile/src/main/scala/com/github/shadowsocks/acl/AclSyncJob.scala similarity index 91% rename from src/main/scala/com/github/shadowsocks/acl/AclSyncJob.scala rename to mobile/src/main/scala/com/github/shadowsocks/acl/AclSyncJob.scala index c5082f30..54129aef 100644 --- a/src/main/scala/com/github/shadowsocks/acl/AclSyncJob.scala +++ b/mobile/src/main/scala/com/github/shadowsocks/acl/AclSyncJob.scala @@ -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 => diff --git a/src/main/scala/com/github/shadowsocks/acl/CustomRulesFragment.scala b/mobile/src/main/scala/com/github/shadowsocks/acl/CustomRulesFragment.scala similarity index 93% rename from src/main/scala/com/github/shadowsocks/acl/CustomRulesFragment.scala rename to mobile/src/main/scala/com/github/shadowsocks/acl/CustomRulesFragment.scala index 220dc804..0bf6a523 100644 --- a/src/main/scala/com/github/shadowsocks/acl/CustomRulesFragment.scala +++ b/mobile/src/main/scala/com/github/shadowsocks/acl/CustomRulesFragment.scala @@ -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() diff --git a/src/main/scala/com/github/shadowsocks/acl/DonaldTrump.scala b/mobile/src/main/scala/com/github/shadowsocks/acl/DonaldTrump.scala similarity index 100% rename from src/main/scala/com/github/shadowsocks/acl/DonaldTrump.scala rename to mobile/src/main/scala/com/github/shadowsocks/acl/DonaldTrump.scala diff --git a/src/main/scala/com/github/shadowsocks/acl/Subnet.scala b/mobile/src/main/scala/com/github/shadowsocks/acl/Subnet.scala similarity index 55% rename from src/main/scala/com/github/shadowsocks/acl/Subnet.scala rename to mobile/src/main/scala/com/github/shadowsocks/acl/Subnet.scala index d7d4cc0b..71e6d67f 100644 --- a/src/main/scala/com/github/shadowsocks/acl/Subnet.scala +++ b/mobile/src/main/scala/com/github/shadowsocks/acl/Subnet.scala @@ -1,6 +1,7 @@ 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() } diff --git a/src/main/scala/com/github/shadowsocks/database/DBHelper.scala b/mobile/src/main/scala/com/github/shadowsocks/database/DBHelper.scala similarity index 83% rename from src/main/scala/com/github/shadowsocks/database/DBHelper.scala rename to mobile/src/main/scala/com/github/shadowsocks/database/DBHelper.scala index 9e4e55b5..155ddf28 100644 --- a/src/main/scala/com/github/shadowsocks/database/DBHelper.scala +++ b/mobile/src/main/scala/com/github/shadowsocks/database/DBHelper.scala @@ -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) diff --git a/src/main/scala/com/github/shadowsocks/database/Profile.scala b/mobile/src/main/scala/com/github/shadowsocks/database/Profile.scala similarity index 81% rename from src/main/scala/com/github/shadowsocks/database/Profile.scala rename to mobile/src/main/scala/com/github/shadowsocks/database/Profile.scala index dac6e203..53581e85 100644 --- a/src/main/scala/com/github/shadowsocks/database/Profile.scala +++ b/mobile/src/main/scala/com/github/shadowsocks/database/Profile.scala @@ -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) } } diff --git a/src/main/scala/com/github/shadowsocks/database/ProfileManager.scala b/mobile/src/main/scala/com/github/shadowsocks/database/ProfileManager.scala similarity index 100% rename from src/main/scala/com/github/shadowsocks/database/ProfileManager.scala rename to mobile/src/main/scala/com/github/shadowsocks/database/ProfileManager.scala diff --git a/mobile/src/main/scala/com/github/shadowsocks/plugin/NativePlugin.scala b/mobile/src/main/scala/com/github/shadowsocks/plugin/NativePlugin.scala new file mode 100644 index 00000000..34c65a79 --- /dev/null +++ b/mobile/src/main/scala/com/github/shadowsocks/plugin/NativePlugin.scala @@ -0,0 +1,14 @@ +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 +} diff --git a/mobile/src/main/scala/com/github/shadowsocks/plugin/NoPlugin.scala b/mobile/src/main/scala/com/github/shadowsocks/plugin/NoPlugin.scala new file mode 100644 index 00000000..7081b79c --- /dev/null +++ b/mobile/src/main/scala/com/github/shadowsocks/plugin/NoPlugin.scala @@ -0,0 +1,11 @@ +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) +} diff --git a/mobile/src/main/scala/com/github/shadowsocks/plugin/Plugin.scala b/mobile/src/main/scala/com/github/shadowsocks/plugin/Plugin.scala new file mode 100644 index 00000000..f206034e --- /dev/null +++ b/mobile/src/main/scala/com/github/shadowsocks/plugin/Plugin.scala @@ -0,0 +1,15 @@ +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 +} diff --git a/mobile/src/main/scala/com/github/shadowsocks/plugin/PluginConfiguration.scala b/mobile/src/main/scala/com/github/shadowsocks/plugin/PluginConfiguration.scala new file mode 100644 index 00000000..cbb37f0a --- /dev/null +++ b/mobile/src/main/scala/com/github/shadowsocks/plugin/PluginConfiguration.scala @@ -0,0 +1,48 @@ +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") + } +} diff --git a/mobile/src/main/scala/com/github/shadowsocks/plugin/PluginManager.scala b/mobile/src/main/scala/com/github/shadowsocks/plugin/PluginManager.scala new file mode 100644 index 00000000..7f8f536c --- /dev/null +++ b/mobile/src/main/scala/com/github/shadowsocks/plugin/PluginManager.scala @@ -0,0 +1,165 @@ +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 + } +} diff --git a/mobile/src/main/scala/com/github/shadowsocks/plugin/ResolvedPlugin.scala b/mobile/src/main/scala/com/github/shadowsocks/plugin/ResolvedPlugin.scala new file mode 100644 index 00000000..ac23b390 --- /dev/null +++ b/mobile/src/main/scala/com/github/shadowsocks/plugin/ResolvedPlugin.scala @@ -0,0 +1,24 @@ +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) +} diff --git a/mobile/src/main/scala/com/github/shadowsocks/preference/BottomSheetPreferenceDialogFragment.scala b/mobile/src/main/scala/com/github/shadowsocks/preference/BottomSheetPreferenceDialogFragment.scala new file mode 100644 index 00000000..1cb28118 --- /dev/null +++ b/mobile/src/main/scala/com/github/shadowsocks/preference/BottomSheetPreferenceDialogFragment.scala @@ -0,0 +1,92 @@ +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)) + } +} diff --git a/mobile/src/main/scala/com/github/shadowsocks/preference/IconListPreference.scala b/mobile/src/main/scala/com/github/shadowsocks/preference/IconListPreference.scala new file mode 100644 index 00000000..86591147 --- /dev/null +++ b/mobile/src/main/scala/com/github/shadowsocks/preference/IconListPreference.scala @@ -0,0 +1,68 @@ +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() + } +} diff --git a/src/main/scala/com/github/shadowsocks/preferences/KcpCliPreferenceDialogFragment.scala b/mobile/src/main/scala/com/github/shadowsocks/preference/PluginConfigurationDialogFragment.scala similarity index 65% rename from src/main/scala/com/github/shadowsocks/preferences/KcpCliPreferenceDialogFragment.scala rename to mobile/src/main/scala/com/github/shadowsocks/preference/PluginConfigurationDialogFragment.scala index 05319d9c..bc6046f0 100644 --- a/src/main/scala/com/github/shadowsocks/preferences/KcpCliPreferenceDialogFragment.scala +++ b/mobile/src/main/scala/com/github/shadowsocks/preference/PluginConfigurationDialogFragment.scala @@ -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) } } diff --git a/src/main/scala/com/github/shadowsocks/utils/CloseUtils.scala b/mobile/src/main/scala/com/github/shadowsocks/utils/CloseUtils.scala similarity index 100% rename from src/main/scala/com/github/shadowsocks/utils/CloseUtils.scala rename to mobile/src/main/scala/com/github/shadowsocks/utils/CloseUtils.scala diff --git a/src/main/scala/com/github/shadowsocks/utils/Constants.scala b/mobile/src/main/scala/com/github/shadowsocks/utils/Constants.scala similarity index 94% rename from src/main/scala/com/github/shadowsocks/utils/Constants.scala rename to mobile/src/main/scala/com/github/shadowsocks/utils/Constants.scala index ea504bc0..d1d9fe21 100644 --- a/src/main/scala/com/github/shadowsocks/utils/Constants.scala +++ b/mobile/src/main/scala/com/github/shadowsocks/utils/Constants.scala @@ -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" diff --git a/src/main/scala/com/github/shadowsocks/utils/IOUtils.scala b/mobile/src/main/scala/com/github/shadowsocks/utils/IOUtils.scala similarity index 83% rename from src/main/scala/com/github/shadowsocks/utils/IOUtils.scala rename to mobile/src/main/scala/com/github/shadowsocks/utils/IOUtils.scala index ae04ef85..993a4cf6 100644 --- a/src/main/scala/com/github/shadowsocks/utils/IOUtils.scala +++ b/mobile/src/main/scala/com/github/shadowsocks/utils/IOUtils.scala @@ -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) + } } diff --git a/src/main/scala/com/github/shadowsocks/utils/Parser.scala b/mobile/src/main/scala/com/github/shadowsocks/utils/Parser.scala similarity index 54% rename from src/main/scala/com/github/shadowsocks/utils/Parser.scala rename to mobile/src/main/scala/com/github/shadowsocks/utils/Parser.scala index e9666fda..2645a809 100644 --- a/src/main/scala/com/github/shadowsocks/utils/Parser.scala +++ b/mobile/src/main/scala/com/github/shadowsocks/utils/Parser.scala @@ -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) } diff --git a/src/main/scala/com/github/shadowsocks/utils/TaskerSettings.scala b/mobile/src/main/scala/com/github/shadowsocks/utils/TaskerSettings.scala similarity index 100% rename from src/main/scala/com/github/shadowsocks/utils/TaskerSettings.scala rename to mobile/src/main/scala/com/github/shadowsocks/utils/TaskerSettings.scala diff --git a/src/main/scala/com/github/shadowsocks/utils/TcpFastOpen.scala b/mobile/src/main/scala/com/github/shadowsocks/utils/TcpFastOpen.scala similarity index 96% rename from src/main/scala/com/github/shadowsocks/utils/TcpFastOpen.scala rename to mobile/src/main/scala/com/github/shadowsocks/utils/TcpFastOpen.scala index ffb7606c..033fc03d 100644 --- a/src/main/scala/com/github/shadowsocks/utils/TcpFastOpen.scala +++ b/mobile/src/main/scala/com/github/shadowsocks/utils/TcpFastOpen.scala @@ -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) { diff --git a/src/main/scala/com/github/shadowsocks/utils/TrafficMonitor.scala b/mobile/src/main/scala/com/github/shadowsocks/utils/TrafficMonitor.scala similarity index 99% rename from src/main/scala/com/github/shadowsocks/utils/TrafficMonitor.scala rename to mobile/src/main/scala/com/github/shadowsocks/utils/TrafficMonitor.scala index d407356c..6e4d7927 100644 --- a/src/main/scala/com/github/shadowsocks/utils/TrafficMonitor.scala +++ b/mobile/src/main/scala/com/github/shadowsocks/utils/TrafficMonitor.scala @@ -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 } diff --git a/src/main/scala/com/github/shadowsocks/utils/TrafficMonitorThread.scala b/mobile/src/main/scala/com/github/shadowsocks/utils/TrafficMonitorThread.scala similarity index 94% rename from src/main/scala/com/github/shadowsocks/utils/TrafficMonitorThread.scala rename to mobile/src/main/scala/com/github/shadowsocks/utils/TrafficMonitorThread.scala index 6efa39cf..0edb656f 100644 --- a/src/main/scala/com/github/shadowsocks/utils/TrafficMonitorThread.scala +++ b/mobile/src/main/scala/com/github/shadowsocks/utils/TrafficMonitorThread.scala @@ -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 => diff --git a/src/main/scala/com/github/shadowsocks/utils/Typefaces.scala b/mobile/src/main/scala/com/github/shadowsocks/utils/Typefaces.scala similarity index 100% rename from src/main/scala/com/github/shadowsocks/utils/Typefaces.scala rename to mobile/src/main/scala/com/github/shadowsocks/utils/Typefaces.scala diff --git a/src/main/scala/com/github/shadowsocks/utils/Utils.scala b/mobile/src/main/scala/com/github/shadowsocks/utils/Utils.scala similarity index 67% rename from src/main/scala/com/github/shadowsocks/utils/Utils.scala rename to mobile/src/main/scala/com/github/shadowsocks/utils/Utils.scala index 348fc6a4..a97f6f25 100644 --- a/src/main/scala/com/github/shadowsocks/utils/Utils.scala +++ b/mobile/src/main/scala/com/github/shadowsocks/utils/Utils.scala @@ -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) { diff --git a/src/main/scala/com/github/shadowsocks/widget/BoundedCardView.scala b/mobile/src/main/scala/com/github/shadowsocks/widget/BoundedCardView.scala similarity index 100% rename from src/main/scala/com/github/shadowsocks/widget/BoundedCardView.scala rename to mobile/src/main/scala/com/github/shadowsocks/widget/BoundedCardView.scala diff --git a/src/main/scala/com/github/shadowsocks/widget/BoundedGridLayout.scala b/mobile/src/main/scala/com/github/shadowsocks/widget/BoundedGridLayout.scala similarity index 100% rename from src/main/scala/com/github/shadowsocks/widget/BoundedGridLayout.scala rename to mobile/src/main/scala/com/github/shadowsocks/widget/BoundedGridLayout.scala diff --git a/src/main/scala/com/github/shadowsocks/widget/BoundedView.scala b/mobile/src/main/scala/com/github/shadowsocks/widget/BoundedView.scala similarity index 100% rename from src/main/scala/com/github/shadowsocks/widget/BoundedView.scala rename to mobile/src/main/scala/com/github/shadowsocks/widget/BoundedView.scala diff --git a/src/main/scala/com/github/shadowsocks/widget/UndoSnackbarManager.scala b/mobile/src/main/scala/com/github/shadowsocks/widget/UndoSnackbarManager.scala similarity index 100% rename from src/main/scala/com/github/shadowsocks/widget/UndoSnackbarManager.scala rename to mobile/src/main/scala/com/github/shadowsocks/widget/UndoSnackbarManager.scala diff --git a/src/main/scala/scala/collection/mutable/SortedList.scala b/mobile/src/main/scala/scala/collection/mutable/SortedList.scala similarity index 100% rename from src/main/scala/scala/collection/mutable/SortedList.scala rename to mobile/src/main/scala/scala/collection/mutable/SortedList.scala diff --git a/plugin/CHANGES.md b/plugin/CHANGES.md new file mode 100644 index 00000000..78ee27d1 --- /dev/null +++ b/plugin/CHANGES.md @@ -0,0 +1,4 @@ +* 0.0.2: + * Add `getOrDefault` to `PluginOptions`; + * Update support library version to 25.1.1. +* 0.0.1: Initial release. diff --git a/plugin/README.md b/plugin/README.md new file mode 100644 index 00000000..cfd062f8 --- /dev/null +++ b/plugin/README.md @@ -0,0 +1,168 @@ +# 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 + + ... + + ... + + + + + + + + + + + + ... + + +``` + +### 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 + + ... + + ... + + + + + + + + ... + + +``` + +#### 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 + + ... + + ... + + + + + + + + ... + + +``` + +Great. Now your plugin is ready to use. diff --git a/plugin/build.sbt b/plugin/build.sbt new file mode 100644 index 00000000..ca3bc9b4 --- /dev/null +++ b/plugin/build.sbt @@ -0,0 +1,32 @@ +enablePlugins(AndroidLib) +android.useSupportVectors + +name := "plugin" +version := "0.0.2" + +pomExtra in Global := { + https://github.com/shadowsocks/shadowsocks-android + + + GPLv3 + https://www.gnu.org/licenses/gpl-3.0.html + + + + scm:git:git://github.com/shadowsocks/shadowsocks-android.git + scm:git:git@github.com:shadowsocks/shadowsocks-android.git + github.com/shadowsocks/shadowsocks-android + + + + madeye + Max Lv + https://github.com/madeye + + + Mygod + Mygod Studio + https://github.com/Mygod + + +} diff --git a/plugin/doc.md b/plugin/doc.md new file mode 100644 index 00000000..b88904b0 --- /dev/null +++ b/plugin/doc.md @@ -0,0 +1,230 @@ +# 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 + + ... + + ... + + + + + + + + ... + + +``` + +## 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 + + ... + + ... + + + + + + + + ... + + +``` + +# 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 + + ... + + ... + + + + + + + + + + + + ... + + +``` + +## 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) + +``` + +``` diff --git a/plugin/src/main/AndroidManifest.xml b/plugin/src/main/AndroidManifest.xml new file mode 100644 index 00000000..bef42e78 --- /dev/null +++ b/plugin/src/main/AndroidManifest.xml @@ -0,0 +1,11 @@ + + + + + + diff --git a/plugin/src/main/java/com/github/shadowsocks/plugin/PluginContract.java b/plugin/src/main/java/com/github/shadowsocks/plugin/PluginContract.java new file mode 100644 index 00000000..e716d2cb --- /dev/null +++ b/plugin/src/main/java/com/github/shadowsocks/plugin/PluginContract.java @@ -0,0 +1,102 @@ +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= Enable obfuscating: HTTP or TLS (Experimental). + * obfs-host= 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"; +} diff --git a/plugin/src/main/java/com/github/shadowsocks/plugin/PluginOptions.java b/plugin/src/main/java/com/github/shadowsocks/plugin/PluginOptions.java new file mode 100644 index 00000000..7aa436db --- /dev/null +++ b/plugin/src/main/java/com/github/shadowsocks/plugin/PluginOptions.java @@ -0,0 +1,109 @@ +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 { + 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 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); + } +} diff --git a/plugin/src/main/res/layout/preference_dialog_edittext.xml b/plugin/src/main/res/layout/preference_dialog_edittext.xml new file mode 100644 index 00000000..34c8f873 --- /dev/null +++ b/plugin/src/main/res/layout/preference_dialog_edittext.xml @@ -0,0 +1,40 @@ + + + + + + + + diff --git a/src/main/res/layout/toolbar_light_dark.xml b/plugin/src/main/res/layout/toolbar_light_dark.xml similarity index 100% rename from src/main/res/layout/toolbar_light_dark.xml rename to plugin/src/main/res/layout/toolbar_light_dark.xml diff --git a/plugin/src/main/res/values-v21/styles.xml b/plugin/src/main/res/values-v21/styles.xml new file mode 100644 index 00000000..624881d2 --- /dev/null +++ b/plugin/src/main/res/values-v21/styles.xml @@ -0,0 +1,4 @@ + + + - - - - @@ -39,20 +32,14 @@ bold ?android:attr/textColorPrimary - - - - diff --git a/src/main/scala/be/mygod/preference/DialogPreferencePlus.scala b/plugin/src/main/scala/be/mygod/preference/DialogPreferencePlus.scala similarity index 100% rename from src/main/scala/be/mygod/preference/DialogPreferencePlus.scala rename to plugin/src/main/scala/be/mygod/preference/DialogPreferencePlus.scala diff --git a/src/main/scala/be/mygod/preference/EditTextPreference.scala b/plugin/src/main/scala/be/mygod/preference/EditTextPreference.scala similarity index 86% rename from src/main/scala/be/mygod/preference/EditTextPreference.scala rename to plugin/src/main/scala/be/mygod/preference/EditTextPreference.scala index 88da9663..e23d261a 100644 --- a/src/main/scala/be/mygod/preference/EditTextPreference.scala +++ b/plugin/src/main/scala/be/mygod/preference/EditTextPreference.scala @@ -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 = { diff --git a/src/main/scala/be/mygod/preference/EditTextPreferenceDialogFragment.scala b/plugin/src/main/scala/be/mygod/preference/EditTextPreferenceDialogFragment.scala similarity index 81% rename from src/main/scala/be/mygod/preference/EditTextPreferenceDialogFragment.scala rename to plugin/src/main/scala/be/mygod/preference/EditTextPreferenceDialogFragment.scala index 7a03eea0..dadf7893 100644 --- a/src/main/scala/be/mygod/preference/EditTextPreferenceDialogFragment.scala +++ b/plugin/src/main/scala/be/mygod/preference/EditTextPreferenceDialogFragment.scala @@ -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 diff --git a/src/main/scala/be/mygod/preference/NumberPickerPreference.scala b/plugin/src/main/scala/be/mygod/preference/NumberPickerPreference.scala similarity index 98% rename from src/main/scala/be/mygod/preference/NumberPickerPreference.scala rename to plugin/src/main/scala/be/mygod/preference/NumberPickerPreference.scala index b321db1d..9b303670 100644 --- a/src/main/scala/be/mygod/preference/NumberPickerPreference.scala +++ b/plugin/src/main/scala/be/mygod/preference/NumberPickerPreference.scala @@ -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 { diff --git a/src/main/scala/be/mygod/preference/NumberPickerPreferenceDialogFragment.scala b/plugin/src/main/scala/be/mygod/preference/NumberPickerPreferenceDialogFragment.scala similarity index 100% rename from src/main/scala/be/mygod/preference/NumberPickerPreferenceDialogFragment.scala rename to plugin/src/main/scala/be/mygod/preference/NumberPickerPreferenceDialogFragment.scala diff --git a/src/main/scala/be/mygod/preference/PreferenceCategory.scala b/plugin/src/main/scala/be/mygod/preference/PreferenceCategory.scala similarity index 100% rename from src/main/scala/be/mygod/preference/PreferenceCategory.scala rename to plugin/src/main/scala/be/mygod/preference/PreferenceCategory.scala diff --git a/src/main/scala/be/mygod/preference/PreferenceFragment.scala b/plugin/src/main/scala/be/mygod/preference/PreferenceFragment.scala similarity index 96% rename from src/main/scala/be/mygod/preference/PreferenceFragment.scala rename to plugin/src/main/scala/be/mygod/preference/PreferenceFragment.scala index 821aeb5d..95139398 100644 --- a/src/main/scala/be/mygod/preference/PreferenceFragment.scala +++ b/plugin/src/main/scala/be/mygod/preference/PreferenceFragment.scala @@ -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() diff --git a/src/main/scala/be/mygod/preference/PreferenceGroupAdapter.scala b/plugin/src/main/scala/be/mygod/preference/PreferenceGroupAdapter.scala similarity index 99% rename from src/main/scala/be/mygod/preference/PreferenceGroupAdapter.scala rename to plugin/src/main/scala/be/mygod/preference/PreferenceGroupAdapter.scala index 41934e17..8819ca37 100644 --- a/src/main/scala/be/mygod/preference/PreferenceGroupAdapter.scala +++ b/plugin/src/main/scala/be/mygod/preference/PreferenceGroupAdapter.scala @@ -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 diff --git a/src/main/scala/be/mygod/preference/SummaryPreference.scala b/plugin/src/main/scala/be/mygod/preference/SummaryPreference.scala similarity index 100% rename from src/main/scala/be/mygod/preference/SummaryPreference.scala rename to plugin/src/main/scala/be/mygod/preference/SummaryPreference.scala diff --git a/plugin/src/main/scala/com/github/shadowsocks/plugin/ConfigurationActivity.scala b/plugin/src/main/scala/com/github/shadowsocks/plugin/ConfigurationActivity.scala new file mode 100644 index 00000000..3759c868 --- /dev/null +++ b/plugin/src/main/scala/com/github/shadowsocks/plugin/ConfigurationActivity.scala @@ -0,0 +1,51 @@ +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: + * + *

    <manifest>
    +  *    ...
    +  *    <application>
    +  *        ...
    +  *        <activity android:name=".ConfigureActivity">
    +  *            <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>
    + * + * @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() + } +} diff --git a/plugin/src/main/scala/com/github/shadowsocks/plugin/HelpActivity.scala b/plugin/src/main/scala/com/github/shadowsocks/plugin/HelpActivity.scala new file mode 100644 index 00000000..591833d9 --- /dev/null +++ b/plugin/src/main/scala/com/github/shadowsocks/plugin/HelpActivity.scala @@ -0,0 +1,29 @@ +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: + * + *
    <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>
    + * + * @author Mygod + */ +trait HelpActivity extends OptionsCapableActivity { + // HelpActivity can choose to ignore options + override protected def onInitializePluginOptions(options: PluginOptions): Unit = () +} diff --git a/plugin/src/main/scala/com/github/shadowsocks/plugin/HelpCallback.scala b/plugin/src/main/scala/com/github/shadowsocks/plugin/HelpCallback.scala new file mode 100644 index 00000000..76c11b09 --- /dev/null +++ b/plugin/src/main/scala/com/github/shadowsocks/plugin/HelpCallback.scala @@ -0,0 +1,20 @@ +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() + } +} diff --git a/plugin/src/main/scala/com/github/shadowsocks/plugin/NativePluginProvider.scala b/plugin/src/main/scala/com/github/shadowsocks/plugin/NativePluginProvider.scala new file mode 100644 index 00000000..412ab0c1 --- /dev/null +++ b/plugin/src/main/scala/com/github/shadowsocks/plugin/NativePluginProvider.scala @@ -0,0 +1,83 @@ +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: + * + *
    <manifest>
    +  *    ...
    +  *    <application>
    +  *        ...
    +  *        <provider android:name="com.github.shadowsocks.$PLUGIN_ID.BinaryProvider"
    +  *                     android:authorities="com.github.shadowsocks.plugin.$PLUGIN_ID.BinaryProvider">
    +  *            <intent-filter>
    +  *                <category android:name="com.github.shadowsocks.plugin.ACTION_NATIVE_PLUGIN" />
    +  *            </intent-filter>
    +  *        </provider>
    +  *        ...
    +  *    </application>
    +  *</manifest>
    + * + * @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 = ??? +} diff --git a/plugin/src/main/scala/com/github/shadowsocks/plugin/OptionsCapableActivity.scala b/plugin/src/main/scala/com/github/shadowsocks/plugin/OptionsCapableActivity.scala new file mode 100644 index 00000000..c2cb0f39 --- /dev/null +++ b/plugin/src/main/scala/com/github/shadowsocks/plugin/OptionsCapableActivity.scala @@ -0,0 +1,32 @@ +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() + } +} diff --git a/plugin/src/main/scala/com/github/shadowsocks/plugin/PathProvider.scala b/plugin/src/main/scala/com/github/shadowsocks/plugin/PathProvider.scala new file mode 100644 index 00000000..536e94e8 --- /dev/null +++ b/plugin/src/main/scala/com/github/shadowsocks/plugin/PathProvider.scala @@ -0,0 +1,39 @@ +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 + } +} diff --git a/project/plugins.sbt b/project/plugins.sbt index dd47db9f..b135a886 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,5 +1,9 @@ -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 diff --git a/src/main/jni/shadowsocks-libev b/src/main/jni/shadowsocks-libev deleted file mode 160000 index 3b4af24f..00000000 --- a/src/main/jni/shadowsocks-libev +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 3b4af24fecae932abe02830321af6f6cb54f72e2 diff --git a/src/main/res/values-v21/styles.xml b/src/main/res/values-v21/styles.xml deleted file mode 100644 index 1050141b..00000000 --- a/src/main/res/values-v21/styles.xml +++ /dev/null @@ -1,4 +0,0 @@ - - -