Explorar el Código

Added AndroidApp project

Rod Hynes hace 11 años
padre
commit
aeb024a106
Se han modificado 49 ficheros con 1840 adiciones y 0 borrados
  1. 6 0
      AndroidApp/.gitignore
  2. 1 0
      AndroidApp/.idea/.name
  3. 23 0
      AndroidApp/.idea/compiler.xml
  4. 3 0
      AndroidApp/.idea/copyright/profiles_settings.xml
  5. 5 0
      AndroidApp/.idea/encodings.xml
  6. 18 0
      AndroidApp/.idea/gradle.xml
  7. 10 0
      AndroidApp/.idea/misc.xml
  8. 10 0
      AndroidApp/.idea/modules.xml
  9. 5 0
      AndroidApp/.idea/scopes/scope_settings.xml
  10. 7 0
      AndroidApp/.idea/vcs.xml
  11. 19 0
      AndroidApp/AndroidApp.iml
  12. 35 0
      AndroidApp/README.md
  13. 1 0
      AndroidApp/app/.gitignore
  14. 85 0
      AndroidApp/app/app.iml
  15. 24 0
      AndroidApp/app/build.gradle
  16. 17 0
      AndroidApp/app/proguard-rules.pro
  17. 13 0
      AndroidApp/app/src/androidTest/java/ca/psiphon/psibot/ApplicationTest.java
  18. 32 0
      AndroidApp/app/src/main/AndroidManifest.xml
  19. 30 0
      AndroidApp/app/src/main/java/ca/psiphon/psibot/App.java
  20. 149 0
      AndroidApp/app/src/main/java/ca/psiphon/psibot/Client.java
  21. 120 0
      AndroidApp/app/src/main/java/ca/psiphon/psibot/Log.java
  22. 105 0
      AndroidApp/app/src/main/java/ca/psiphon/psibot/LogFragment.java
  23. 92 0
      AndroidApp/app/src/main/java/ca/psiphon/psibot/MainActivity.java
  24. 211 0
      AndroidApp/app/src/main/java/ca/psiphon/psibot/Service.java
  25. 122 0
      AndroidApp/app/src/main/java/ca/psiphon/psibot/SocketProtector.java
  26. 102 0
      AndroidApp/app/src/main/java/ca/psiphon/psibot/Tun2Socks.java
  27. 208 0
      AndroidApp/app/src/main/java/ca/psiphon/psibot/Utils.java
  28. BIN
      AndroidApp/app/src/main/res/drawable-hdpi/ic_launcher.png
  29. BIN
      AndroidApp/app/src/main/res/drawable-mdpi/ic_launcher.png
  30. BIN
      AndroidApp/app/src/main/res/drawable-xhdpi/ic_launcher.png
  31. BIN
      AndroidApp/app/src/main/res/drawable-xxhdpi/ic_launcher.png
  32. 7 0
      AndroidApp/app/src/main/res/layout/activity_main.xml
  33. 26 0
      AndroidApp/app/src/main/res/layout/log_entry_row.xml
  34. 8 0
      AndroidApp/app/src/main/res/menu/main.xml
  35. 12 0
      AndroidApp/app/src/main/res/raw/psiphon_config
  36. BIN
      AndroidApp/app/src/main/res/raw/psiphon_tunnel_core_arm
  37. 5 0
      AndroidApp/app/src/main/res/values-v21/styles.xml
  38. 6 0
      AndroidApp/app/src/main/res/values-w820dp/dimens.xml
  39. 5 0
      AndroidApp/app/src/main/res/values/dimens.xml
  40. 8 0
      AndroidApp/app/src/main/res/values/strings.xml
  41. 8 0
      AndroidApp/app/src/main/res/values/styles.xml
  42. 4 0
      AndroidApp/app/src/main/res/values/symbols.xml
  43. 19 0
      AndroidApp/build.gradle
  44. 18 0
      AndroidApp/gradle.properties
  45. BIN
      AndroidApp/gradle/wrapper/gradle-wrapper.jar
  46. 6 0
      AndroidApp/gradle/wrapper/gradle-wrapper.properties
  47. 164 0
      AndroidApp/gradlew
  48. 90 0
      AndroidApp/gradlew.bat
  49. 1 0
      AndroidApp/settings.gradle

+ 6 - 0
AndroidApp/.gitignore

@@ -0,0 +1,6 @@
+.gradle
+/local.properties
+/.idea/workspace.xml
+/.idea/libraries
+.DS_Store
+/build

+ 1 - 0
AndroidApp/.idea/.name

@@ -0,0 +1 @@
+Psibot

+ 23 - 0
AndroidApp/.idea/compiler.xml

@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="CompilerConfiguration">
+    <option name="DEFAULT_COMPILER" value="Javac" />
+    <resourceExtensions />
+    <wildcardResourcePatterns>
+      <entry name="!?*.java" />
+      <entry name="!?*.form" />
+      <entry name="!?*.class" />
+      <entry name="!?*.groovy" />
+      <entry name="!?*.scala" />
+      <entry name="!?*.flex" />
+      <entry name="!?*.kt" />
+      <entry name="!?*.clj" />
+    </wildcardResourcePatterns>
+    <annotationProcessing>
+      <profile default="true" name="Default" enabled="false">
+        <processorPath useClasspath="true" />
+      </profile>
+    </annotationProcessing>
+  </component>
+</project>
+

+ 3 - 0
AndroidApp/.idea/copyright/profiles_settings.xml

@@ -0,0 +1,3 @@
+<component name="CopyrightManager">
+  <settings default="" />
+</component>

+ 5 - 0
AndroidApp/.idea/encodings.xml

@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="Encoding" useUTFGuessing="true" native2AsciiForPropertiesFiles="false" />
+</project>
+

+ 18 - 0
AndroidApp/.idea/gradle.xml

@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="GradleSettings">
+    <option name="linkedExternalProjectsSettings">
+      <GradleProjectSettings>
+        <option name="distributionType" value="DEFAULT_WRAPPED" />
+        <option name="externalProjectPath" value="$PROJECT_DIR$" />
+        <option name="modules">
+          <set>
+            <option value="$PROJECT_DIR$" />
+            <option value="$PROJECT_DIR$/app" />
+          </set>
+        </option>
+      </GradleProjectSettings>
+    </option>
+  </component>
+</project>
+

+ 10 - 0
AndroidApp/.idea/misc.xml

@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="EntryPointsManager">
+    <entry_points version="2.0" />
+  </component>
+  <component name="ProjectRootManager" version="2" languageLevel="JDK_1_6" assert-keyword="true" jdk-15="true" project-jdk-name="JDK" project-jdk-type="JavaSDK">
+    <output url="file://$PROJECT_DIR$/build/classes" />
+  </component>
+</project>
+

+ 10 - 0
AndroidApp/.idea/modules.xml

@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="ProjectModuleManager">
+    <modules>
+      <module fileurl="file://$PROJECT_DIR$/AndroidApp.iml" filepath="$PROJECT_DIR$/AndroidApp.iml" />
+      <module fileurl="file://$PROJECT_DIR$/app/app.iml" filepath="$PROJECT_DIR$/app/app.iml" />
+    </modules>
+  </component>
+</project>
+

+ 5 - 0
AndroidApp/.idea/scopes/scope_settings.xml

@@ -0,0 +1,5 @@
+<component name="DependencyValidationManager">
+  <state>
+    <option name="SKIP_IMPORT_STATEMENTS" value="false" />
+  </state>
+</component>

+ 7 - 0
AndroidApp/.idea/vcs.xml

@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="VcsDirectoryMappings">
+    <mapping directory="" vcs="" />
+  </component>
+</project>
+

+ 19 - 0
AndroidApp/AndroidApp.iml

@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<module external.linked.project.path="$MODULE_DIR$" external.root.project.path="$MODULE_DIR$" external.system.id="GRADLE" external.system.module.group="" external.system.module.version="unspecified" type="JAVA_MODULE" version="4">
+  <component name="FacetManager">
+    <facet type="java-gradle" name="Java-Gradle">
+      <configuration>
+        <option name="BUILD_FOLDER_PATH" value="$MODULE_DIR$/build" />
+      </configuration>
+    </facet>
+  </component>
+  <component name="NewModuleRootManager" inherit-compiler-output="true">
+    <exclude-output />
+    <content url="file://$MODULE_DIR$">
+      <excludeFolder url="file://$MODULE_DIR$/.gradle" />
+    </content>
+    <orderEntry type="inheritedJdk" />
+    <orderEntry type="sourceFolder" forTests="false" />
+  </component>
+</module>
+

+ 35 - 0
AndroidApp/README.md

@@ -0,0 +1,35 @@
+Psibot README
+================================================================================
+
+Overview
+--------------------------------------------------------------------------------
+
+Psibot is a sample app that demonstrates embedding the Psiphon Go client in
+an Android app. Psibot uses the Android VpnService API to route all device
+traffic through tun2socks and in turn through Psiphon.
+
+Status
+--------------------------------------------------------------------------------
+
+* Incomplete
+  * Android app code builds but is not tested.
+  * Go client builds and runs (tested via adb shell).
+  * Go client support for protect sockets (VpnService) must be implemented.
+
+Native libraries
+--------------------------------------------------------------------------------
+
+`app\src\main\jniLibs\<platform>\libtun2socks.so` is built from the Psiphon fork of badvpn. Source code is here: [https://bitbucket.org/psiphon/psiphon-circumvention-system/src/default/Android/badvpn/](https://bitbucket.org/psiphon/psiphon-circumvention-system/src/default/Android/badvpn/).
+
+Go client binary and config file
+--------------------------------------------------------------------------------
+
+`app\src\main\res\raw\psiphon_tunnel_core_arm` is built with Go targetting android/arm. At this time, android/arm support is not yet released but
+is available in the development branch.
+
+Install Go from source. The Android instructions are here:
+[https://code.google.com/p/go/source/browse/README?repo=mobile](https://code.google.com/p/go/source/browse/README?repo=mobile).
+
+In summary, download and install the Android NDK, use a script to make a standalone toolchain, and use that toolchain to build android/arm support within the Go source install. Then cross compile as usual.
+
+In `app\src\main\res\raw\psiphon_config`, placeholders must be replaced with valid values.

+ 1 - 0
AndroidApp/app/.gitignore

@@ -0,0 +1 @@
+/build

+ 85 - 0
AndroidApp/app/app.iml

@@ -0,0 +1,85 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<module external.linked.project.path="$MODULE_DIR$" external.root.project.path="$MODULE_DIR$/.." external.system.id="GRADLE" external.system.module.group="AndroidApp" external.system.module.version="unspecified" type="JAVA_MODULE" version="4">
+  <component name="FacetManager">
+    <facet type="android-gradle" name="Android-Gradle">
+      <configuration>
+        <option name="GRADLE_PROJECT_PATH" value=":app" />
+      </configuration>
+    </facet>
+    <facet type="android" name="Android">
+      <configuration>
+        <option name="SELECTED_BUILD_VARIANT" value="debug" />
+        <option name="ASSEMBLE_TASK_NAME" value="assembleDebug" />
+        <option name="COMPILE_JAVA_TASK_NAME" value="compileDebugJava" />
+        <option name="ASSEMBLE_TEST_TASK_NAME" value="assembleDebugTest" />
+        <option name="SOURCE_GEN_TASK_NAME" value="generateDebugSources" />
+        <option name="TEST_SOURCE_GEN_TASK_NAME" value="generateDebugTestSources" />
+        <option name="ALLOW_USER_CONFIGURATION" value="false" />
+        <option name="MANIFEST_FILE_RELATIVE_PATH" value="/src/main/AndroidManifest.xml" />
+        <option name="RES_FOLDER_RELATIVE_PATH" value="/src/main/res" />
+        <option name="RES_FOLDERS_RELATIVE_PATH" value="file://$MODULE_DIR$/src/main/res" />
+        <option name="ASSETS_FOLDER_RELATIVE_PATH" value="/src/main/assets" />
+      </configuration>
+    </facet>
+  </component>
+  <component name="NewModuleRootManager" inherit-compiler-output="false">
+    <output url="file://$MODULE_DIR$/build/intermediates/classes/debug" />
+    <exclude-output />
+    <content url="file://$MODULE_DIR$">
+      <sourceFolder url="file://$MODULE_DIR$/build/generated/source/r/debug" isTestSource="false" generated="true" />
+      <sourceFolder url="file://$MODULE_DIR$/build/generated/source/aidl/debug" isTestSource="false" generated="true" />
+      <sourceFolder url="file://$MODULE_DIR$/build/generated/source/buildConfig/debug" isTestSource="false" generated="true" />
+      <sourceFolder url="file://$MODULE_DIR$/build/generated/source/rs/debug" isTestSource="false" generated="true" />
+      <sourceFolder url="file://$MODULE_DIR$/build/generated/res/rs/debug" type="java-resource" />
+      <sourceFolder url="file://$MODULE_DIR$/build/generated/source/r/test/debug" isTestSource="true" generated="true" />
+      <sourceFolder url="file://$MODULE_DIR$/build/generated/source/aidl/test/debug" isTestSource="true" generated="true" />
+      <sourceFolder url="file://$MODULE_DIR$/build/generated/source/buildConfig/test/debug" isTestSource="true" generated="true" />
+      <sourceFolder url="file://$MODULE_DIR$/build/generated/source/rs/test/debug" isTestSource="true" generated="true" />
+      <sourceFolder url="file://$MODULE_DIR$/build/generated/res/rs/test/debug" type="java-test-resource" />
+      <sourceFolder url="file://$MODULE_DIR$/src/debug/res" type="java-resource" />
+      <sourceFolder url="file://$MODULE_DIR$/src/debug/resources" type="java-resource" />
+      <sourceFolder url="file://$MODULE_DIR$/src/debug/assets" type="java-resource" />
+      <sourceFolder url="file://$MODULE_DIR$/src/debug/aidl" isTestSource="false" />
+      <sourceFolder url="file://$MODULE_DIR$/src/debug/java" isTestSource="false" />
+      <sourceFolder url="file://$MODULE_DIR$/src/debug/jni" isTestSource="false" />
+      <sourceFolder url="file://$MODULE_DIR$/src/debug/rs" isTestSource="false" />
+      <sourceFolder url="file://$MODULE_DIR$/src/main/res" type="java-resource" />
+      <sourceFolder url="file://$MODULE_DIR$/src/main/resources" type="java-resource" />
+      <sourceFolder url="file://$MODULE_DIR$/src/main/assets" type="java-resource" />
+      <sourceFolder url="file://$MODULE_DIR$/src/main/aidl" isTestSource="false" />
+      <sourceFolder url="file://$MODULE_DIR$/src/main/java" isTestSource="false" />
+      <sourceFolder url="file://$MODULE_DIR$/src/main/jni" isTestSource="false" />
+      <sourceFolder url="file://$MODULE_DIR$/src/main/rs" isTestSource="false" />
+      <sourceFolder url="file://$MODULE_DIR$/src/androidTest/res" type="java-test-resource" />
+      <sourceFolder url="file://$MODULE_DIR$/src/androidTest/resources" type="java-test-resource" />
+      <sourceFolder url="file://$MODULE_DIR$/src/androidTest/assets" type="java-test-resource" />
+      <sourceFolder url="file://$MODULE_DIR$/src/androidTest/aidl" isTestSource="true" />
+      <sourceFolder url="file://$MODULE_DIR$/src/androidTest/java" isTestSource="true" />
+      <sourceFolder url="file://$MODULE_DIR$/src/androidTest/jni" isTestSource="true" />
+      <sourceFolder url="file://$MODULE_DIR$/src/androidTest/rs" isTestSource="true" />
+      <excludeFolder url="file://$MODULE_DIR$/build/intermediates/assets" />
+      <excludeFolder url="file://$MODULE_DIR$/build/intermediates/bundles" />
+      <excludeFolder url="file://$MODULE_DIR$/build/intermediates/classes" />
+      <excludeFolder url="file://$MODULE_DIR$/build/intermediates/coverage-instrumented-classes" />
+      <excludeFolder url="file://$MODULE_DIR$/build/intermediates/dependency-cache" />
+      <excludeFolder url="file://$MODULE_DIR$/build/intermediates/dex" />
+      <excludeFolder url="file://$MODULE_DIR$/build/intermediates/dex-cache" />
+      <excludeFolder url="file://$MODULE_DIR$/build/intermediates/incremental" />
+      <excludeFolder url="file://$MODULE_DIR$/build/intermediates/jacoco" />
+      <excludeFolder url="file://$MODULE_DIR$/build/intermediates/javaResources" />
+      <excludeFolder url="file://$MODULE_DIR$/build/intermediates/libs" />
+      <excludeFolder url="file://$MODULE_DIR$/build/intermediates/lint" />
+      <excludeFolder url="file://$MODULE_DIR$/build/intermediates/manifests" />
+      <excludeFolder url="file://$MODULE_DIR$/build/intermediates/ndk" />
+      <excludeFolder url="file://$MODULE_DIR$/build/intermediates/pre-dexed" />
+      <excludeFolder url="file://$MODULE_DIR$/build/intermediates/proguard" />
+      <excludeFolder url="file://$MODULE_DIR$/build/intermediates/res" />
+      <excludeFolder url="file://$MODULE_DIR$/build/intermediates/rs" />
+      <excludeFolder url="file://$MODULE_DIR$/build/intermediates/symbols" />
+      <excludeFolder url="file://$MODULE_DIR$/build/outputs" />
+    </content>
+    <orderEntry type="jdk" jdkName="Android API 21 Platform" jdkType="Android SDK" />
+    <orderEntry type="sourceFolder" forTests="false" />
+  </component>
+</module>
+

+ 24 - 0
AndroidApp/app/build.gradle

@@ -0,0 +1,24 @@
+apply plugin: 'com.android.application'
+
+android {
+    compileSdkVersion 21
+    buildToolsVersion "20.0.0"
+
+    defaultConfig {
+        applicationId "ca.psiphon.psibot"
+        minSdkVersion 16
+        targetSdkVersion 21
+        versionCode 1
+        versionName "1.0"
+    }
+    buildTypes {
+        release {
+            runProguard false
+            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+        }
+    }
+}
+
+dependencies {
+    compile fileTree(dir: 'libs', include: ['*.jar'])
+}

+ 17 - 0
AndroidApp/app/proguard-rules.pro

@@ -0,0 +1,17 @@
+# Add project specific ProGuard rules here.
+# By default, the flags in this file are appended to flags specified
+# in /Applications/Android Studio.app/sdk/tools/proguard/proguard-android.txt
+# You can edit the include path and order by changing the proguardFiles
+# directive in build.gradle.
+#
+# For more details, see
+#   http://developer.android.com/guide/developing/tools/proguard.html
+
+# Add any project specific keep options here:
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+#   public *;
+#}

+ 13 - 0
AndroidApp/app/src/androidTest/java/ca/psiphon/psibot/ApplicationTest.java

@@ -0,0 +1,13 @@
+package ca.psiphon.psibot;
+
+import android.app.Application;
+import android.test.ApplicationTestCase;
+
+/**
+ * <a href="http://d.android.com/tools/testing/testing_android.html">Testing Fundamentals</a>
+ */
+public class ApplicationTest extends ApplicationTestCase<Application> {
+    public ApplicationTest() {
+        super(Application.class);
+    }
+}

+ 32 - 0
AndroidApp/app/src/main/AndroidManifest.xml

@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="ca.psiphon.psibot" >
+
+    <uses-permission android:name="android.permission.INTERNET"></uses-permission>
+
+    <application
+        android:name="ca.psiphon.psibot.App"
+        android:allowBackup="true"
+        android:icon="@drawable/ic_launcher"
+        android:label="@string/app_name"
+        android:theme="@style/AppTheme" >
+        <activity
+            android:name=".MainActivity"
+            android:label="@string/app_name" >
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.LAUNCHER" />
+            </intent-filter>
+        </activity>
+        <service
+            android:name="ca.psiphon.psibot.Service"
+            android:label="@string/app_name"
+            android:exported="false"
+            android:permission="android.permission.BIND_VPN_SERVICE" >
+            <intent-filter>
+                <action android:name="android.net.VpnService"/>
+            </intent-filter>
+        </service>
+    </application>
+
+</manifest>

+ 30 - 0
AndroidApp/app/src/main/java/ca/psiphon/psibot/App.java

@@ -0,0 +1,30 @@
+/*
+ * Copyright (c) 2014, Psiphon Inc.
+ * All rights reserved.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package ca.psiphon.psibot;
+
+import android.app.Application;
+
+public class App extends Application {
+
+    @Override
+    public void onCreate() {
+        Log.initialize();
+    }
+}

+ 149 - 0
AndroidApp/app/src/main/java/ca/psiphon/psibot/Client.java

@@ -0,0 +1,149 @@
+/*
+ * Copyright (c) 2014, Psiphon Inc.
+ * All rights reserved.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package ca.psiphon.psibot;
+
+import android.content.Context;
+import android.os.Build;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Scanner;
+import java.util.concurrent.CountDownLatch;
+
+public class Client {
+
+    private final Context mContext;
+    private File mRootDirectory;
+    private File mExecutableFile;
+    private File mConfigFile;
+    private Process mProcess;
+    private Thread mThread;
+    private int mLocalSocksProxyPort;
+    private int mLocalHttpProxyPort;
+    private List<String> mHomePages;
+
+    public Client(Context context) {
+        mContext = context;
+    }
+
+    public void start(final CountDownLatch tunnelStartedSignal) throws Utils.PsibotError {
+        stop();
+        prepareFiles();
+
+        ProcessBuilder processBuilder =
+                new ProcessBuilder(
+                        mExecutableFile.getAbsolutePath(),
+                        "--config", mConfigFile.getAbsolutePath());
+        processBuilder.directory(mRootDirectory);
+
+        try {
+            mProcess = processBuilder.start();
+        } catch (IOException e) {
+            throw new Utils.PsibotError("failed to start client process", e);
+        }
+
+        mThread = new Thread(new Runnable() {
+            @Override
+            public void run() {
+                Scanner stdout = new Scanner(mProcess.getInputStream());
+                while(stdout.hasNextLine()) {
+                    String line = stdout.nextLine();
+                    boolean isTunnelStarted = parseLine(line);
+                    if (isTunnelStarted) {
+                        tunnelStartedSignal.countDown();
+                    }
+                    Log.addEntry(line);
+                }
+                stdout.close();
+            }
+        });
+        mThread.start();
+        Log.addEntry("Psiphon client started");
+    }
+
+    public void stop() {
+        if (mProcess != null) {
+            mProcess.destroy();
+            try {
+                mProcess.waitFor();
+            } catch (InterruptedException e) {
+                Thread.currentThread().interrupt();
+            }
+        }
+        if (mThread != null) {
+            try {
+                mThread.join();
+            } catch (InterruptedException e) {
+                Thread.currentThread().interrupt();
+            }
+        }
+        mProcess = null;
+        mThread = null;
+        Log.addEntry("Psiphon client stopped");
+    }
+
+    public synchronized boolean parseLine(String line) {
+        // TODO: this is based on temporary log line formats
+        final String socksProxy = "SOCKS-PROXY local SOCKS proxy running at address 127.0.0.1:";
+        final String httpProxy = "HTTP-PROXY local HTTP proxy running at address 127.0.0.1:";
+        final String homePage = "HOMEPAGE ";
+        final String tunnelStarted = "TUNNEL tunnel started";
+        int index;
+        if (-1 != (index = line.indexOf(socksProxy))) {
+            mLocalSocksProxyPort = Integer.parseInt(line.substring(index + homePage.length()));
+        } else if (-1 != (index = line.indexOf(httpProxy))) {
+            mLocalHttpProxyPort = Integer.parseInt(line.substring(index + homePage.length()));
+        } else if (-1 != (index = line.indexOf(homePage))) {
+            mHomePages.add(line.substring(index + homePage.length()));
+        } else if (line.contains(tunnelStarted)) {
+            return true;
+        }
+        return false;
+    }
+
+    public synchronized int getLocalSocksProxyPort() {
+        return mLocalSocksProxyPort;
+    }
+
+    public synchronized int getLocalHttpProxyPort() {
+        return mLocalHttpProxyPort;
+    }
+
+    public synchronized List<String> getHomePages() {
+        return mHomePages != null ? new ArrayList<String>(mHomePages) : new ArrayList<String>();
+    }
+
+    private void prepareFiles() throws Utils.PsibotError {
+        mRootDirectory = mContext.getDir("psiphon_tunnel_core", Context.MODE_PRIVATE);
+        mExecutableFile = new File(mRootDirectory, "psiphon_tunnel_core");
+        mConfigFile = new File(mRootDirectory, "psiphon_config");
+        if (0 != Build.CPU_ABI.compareTo("armeabi-v7a")) {
+            throw new Utils.PsibotError("no client binary for this CPU");
+        }
+        try {
+            Utils.writeRawResourceFile(mContext, R.raw.psiphon_tunnel_core_arm, mExecutableFile, true);
+            Utils.writeRawResourceFile(mContext, R.raw.psiphon_config, mConfigFile, false);
+        } catch (IOException e) {
+            throw new Utils.PsibotError("failed to prepare client files", e);
+        }
+    }
+}

+ 120 - 0
AndroidApp/app/src/main/java/ca/psiphon/psibot/Log.java

@@ -0,0 +1,120 @@
+/*
+ * Copyright (c) 2013, Psiphon Inc.
+ * All rights reserved.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package ca.psiphon.psibot;
+
+import android.content.ActivityNotFoundException;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Handler;
+
+import java.util.ArrayList;
+import java.util.Date;
+
+public class Log {
+
+    public static class Entry {
+        public final Date mTimestamp;
+        public final String mMessage;
+
+        public Entry(String message) {
+            mTimestamp = new Date();
+            mMessage = message;
+        }
+    }
+
+    public interface Observer {
+        void onUpdatedRecentEntries();
+    }
+
+    private static final int MAX_ENTRIES = 500;
+
+    private static ArrayList<Entry> mEntries;
+    private static ArrayList<Observer> mObservers;
+    private static Handler mHandler;
+
+    public synchronized static void initialize() {
+        mEntries = new ArrayList<Entry>();
+        mObservers = new ArrayList<Observer>();
+        mHandler = new Handler();
+    }
+
+    public synchronized static void addEntry(String message) {
+        if (message == null) {
+            message = "(null)";
+        }
+
+        final Entry entry = new Entry(message);
+
+        // Update the in-memory entry list on the UI thread (also
+        // notifies any ListView adapters subscribed to that list)
+        mHandler.post(
+                new Runnable() {
+                    @Override
+                    public void run() {
+                        mEntries.add(entry);
+                        while (mEntries.size() > MAX_ENTRIES) {
+                            mEntries.remove(0);
+                        }
+                        for (Observer observer : mObservers) {
+                            observer.onUpdatedRecentEntries();
+                        }
+                    }
+                });
+    }
+
+    public synchronized static int getEntryCount() {
+        return mEntries.size();
+    }
+
+    public synchronized static Entry getEntry(int index) {
+        return mEntries.get(index);
+    }
+
+    public synchronized static void registerObserver(Observer observer) {
+        if (!mObservers.contains(observer)) {
+            mObservers.add(observer);
+        }
+    }
+
+    public synchronized static void unregisterObserver(Observer observer) {
+        mObservers.remove(observer);
+    }
+
+    public synchronized static void composeEmail(Context context) {
+        try {
+            StringBuilder body = new StringBuilder();
+            for (Entry entry : mEntries) {
+                body.append(entry.mTimestamp);
+                body.append(": ");
+                body.append(entry.mMessage);
+                body.append("\n");
+            }
+
+            Intent intent = new Intent(Intent.ACTION_SEND);
+            intent.setType("message/rfc822");
+            intent.putExtra(Intent.EXTRA_EMAIL, new String[]{"feedback+psibot@psiphon.ca"});
+            intent.putExtra(Intent.EXTRA_SUBJECT, "Psibot Logs");
+            intent.putExtra(Intent.EXTRA_TEXT, body.toString());
+            context.startActivity(intent);
+        } catch (ActivityNotFoundException e) {
+            Log.addEntry("compose log email failed: " + e.getMessage());
+        }
+    }
+}

+ 105 - 0
AndroidApp/app/src/main/java/ca/psiphon/psibot/LogFragment.java

@@ -0,0 +1,105 @@
+/*
+ * Copyright (c) 2014, Psiphon Inc.
+ * All rights reserved.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package ca.psiphon.psibot;
+
+import android.app.ListFragment;
+import android.content.Context;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+import android.widget.ListView;
+import android.widget.TextView;
+
+import java.text.DateFormat;
+
+public class LogFragment extends ListFragment implements Log.Observer {
+
+    private LogAdapter mLogAdapter;
+
+    @Override
+    public void onActivityCreated(Bundle savedInstanceState) {
+        super.onActivityCreated(savedInstanceState);
+        mLogAdapter = new LogAdapter(getActivity());
+        setListAdapter(mLogAdapter);
+    }
+
+    @Override
+    public void onResume() {
+        super.onResume();
+        Log.registerObserver(this);
+        getListView().setSelection(mLogAdapter.getCount() - 1);
+        getListView().setTranscriptMode(ListView.TRANSCRIPT_MODE_ALWAYS_SCROLL);
+    }
+
+    @Override
+    public void onPause() {
+        super.onPause();
+        Log.unregisterObserver(this);
+    }
+
+    @Override
+    public void onUpdatedRecentEntries() {
+        mLogAdapter.notifyDataSetChanged();
+        getListView().setSelection(mLogAdapter.getCount() - 1);
+    }
+
+    private static class LogAdapter extends BaseAdapter {
+        private final Context mContext;
+        private final DateFormat mDateFormat;
+
+        public LogAdapter(Context context) {
+            mContext = context;
+            mDateFormat = DateFormat.getDateTimeInstance();
+        }
+
+        @Override
+        public View getView(int position, View view, ViewGroup parent) {
+            if (view == null) {
+                LayoutInflater inflater = (LayoutInflater)mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+                view = inflater.inflate(R.layout.log_entry_row, null);
+            }
+            Log.Entry entry = Log.getEntry(position);
+            if (entry != null) {
+                TextView timestampText = (TextView)view.findViewById(R.id.log_timestamp_text);
+                TextView messageText = (TextView)view.findViewById(R.id.log_message_text);
+                timestampText.setText(mDateFormat.format(entry.mTimestamp));
+                messageText.setText(entry.mMessage);
+            }
+            return view;
+        }
+
+        @Override
+        public int getCount() {
+            return Log.getEntryCount();
+        }
+
+        @Override
+        public Object getItem(int position) {
+            return Log.getEntry(position);
+        }
+
+        @Override
+        public long getItemId(int position) {
+            return position;
+        }
+    }
+}

+ 92 - 0
AndroidApp/app/src/main/java/ca/psiphon/psibot/MainActivity.java

@@ -0,0 +1,92 @@
+/*
+ * Copyright (c) 2014, Psiphon Inc.
+ * All rights reserved.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package ca.psiphon.psibot;
+
+import android.app.Activity;
+import android.content.ActivityNotFoundException;
+import android.content.Intent;
+import android.net.VpnService;
+import android.os.Bundle;
+import android.view.Menu;
+import android.view.MenuItem;
+
+
+public class MainActivity extends Activity {
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.activity_main);
+        if (savedInstanceState == null) {
+            getFragmentManager()
+                .beginTransaction().add(R.id.container, new LogFragment()).commit();
+        }
+    }
+
+    @Override
+    protected void onResume() {
+        startVpn();
+    }
+
+    @Override
+    public boolean onCreateOptionsMenu(Menu menu) {
+        getMenuInflater().inflate(R.menu.main, menu);
+        return true;
+    }
+
+    @Override
+    public boolean onOptionsItemSelected(MenuItem item) {
+        int id = item.getItemId();
+        if (id == R.id.action_email_log) {
+            Log.composeEmail(this);
+            return true;
+        }
+        return super.onOptionsItemSelected(item);
+    }
+
+    protected static final int REQUEST_CODE_PREPARE_VPN = 100;
+
+    protected void startVpn() {
+        if (Service.isRunning()) {
+            return;
+        }
+        try {
+            Intent intent = VpnService.prepare(this);
+            if (intent != null) {
+                startActivityForResult(intent, REQUEST_CODE_PREPARE_VPN);
+            } else {
+                startVpnService();
+            }
+        } catch (ActivityNotFoundException e) {
+            Log.addEntry("prepare VPN failed: " + e.getMessage());
+        }
+    }
+
+    protected void startVpnService() {
+        startService(new Intent(this, Service.class));
+    }
+
+    @Override
+    protected void onActivityResult(int request, int result, Intent data) {
+        if (request == REQUEST_CODE_PREPARE_VPN && result == RESULT_OK) {
+            startVpnService();
+        }
+    }
+}

+ 211 - 0
AndroidApp/app/src/main/java/ca/psiphon/psibot/Service.java

@@ -0,0 +1,211 @@
+/*
+ * Copyright (c) 2014, Psiphon Inc.
+ * All rights reserved.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package ca.psiphon.psibot;
+
+import android.app.Notification;
+import android.app.PendingIntent;
+import android.content.Intent;
+import android.net.VpnService;
+import android.os.ParcelFileDescriptor;
+
+import java.util.Locale;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+public class Service extends VpnService {
+
+    // Note: assumes only one instance of Service
+    private static AtomicBoolean mIsRunning = new AtomicBoolean();
+
+    private Thread mThread;
+    private CountDownLatch mStopSignal;
+
+    public static boolean isRunning() {
+        return mIsRunning.get();
+    }
+
+    @Override
+    public void onCreate() {
+        mIsRunning.set(true);
+        startForeground(R.string.foregroundServiceNotificationId, makeForegroundNotification());
+        startWorking();
+    }
+
+    @Override
+    public void onDestroy() {
+        stopWorking();
+        stopForeground(true);
+        mIsRunning.set(false);
+    }
+
+    private void startWorking() {
+        stopWorking();
+        mStopSignal = new CountDownLatch(1);
+        mThread = new Thread(new Runnable() {
+            @Override
+            public void run() {
+                SocketProtector socketProtector = new SocketProtector(Service.this);
+                Client client = new Client(Service.this);
+                try {
+                    socketProtector.start();
+                    // TODO: what if client local proxies unbind? in this case it's better if Go client keeps its proxies up permanently.
+                    // TODO: monitor tunnel messages and update notification UI when re-connecting, etc.
+                    CountDownLatch tunnelStartedSignal = new CountDownLatch(1);
+                    client.start(tunnelStartedSignal);
+                    while (true) {
+                        if (tunnelStartedSignal.await(100, TimeUnit.MILLISECONDS)) {
+                            break;
+                        }
+                        if (mStopSignal.await(0, TimeUnit.MILLISECONDS)) {
+                            throw new Utils.PsibotError("stopped while waiting tunnel");
+                        }
+                    }
+                    int localSocksProxyPort = client.getLocalSocksProxyPort();
+                    runVpn(localSocksProxyPort);
+                    mStopSignal.await();
+                } catch (Utils.PsibotError e) {
+                    Log.addEntry("Service failed: " + e.getMessage());
+                } catch (InterruptedException e) {
+                    Thread.currentThread().interrupt();
+                }
+                stopVpn();
+                client.stop();
+                socketProtector.stop();
+                stopSelf();
+            }
+        });
+        mThread.start();
+    }
+
+    private void stopWorking() {
+        if (mStopSignal != null) {
+            mStopSignal.countDown();
+        }
+        if (mThread != null) {
+            try {
+                mThread.join();
+            } catch (InterruptedException e) {
+                Thread.currentThread().interrupt();
+            }
+        }
+        mStopSignal = null;
+        mThread = null;
+    }
+
+    private final static String VPN_INTERFACE_NETMASK = "255.255.255.0";
+    private final static int VPN_INTERFACE_MTU = 1500;
+    private final static int UDPGW_SERVER_PORT = 7300;
+
+    private void runVpn(int localSocksProxyPort) throws Utils.PsibotError {
+        Log.addEntry("network type: " + Utils.getNetworkTypeName(this));
+
+        String privateIpAddress = Utils.selectPrivateAddress();
+        if (privateIpAddress == null) {
+            throw new Utils.PsibotError("no private address available");
+        }
+
+        ParcelFileDescriptor vpnInterfaceFileDescriptor = establishVpn(privateIpAddress);
+        Log.addEntry("VPN established");
+
+        String socksServerAddress = "127.0.0.1:" + Integer.toString(localSocksProxyPort);
+        String udpgwServerAddress = "127.0.0.1:" + Integer.toString(UDPGW_SERVER_PORT);
+        Tun2Socks.start(
+                this,
+                vpnInterfaceFileDescriptor,
+                VPN_INTERFACE_MTU,
+                Utils.getPrivateAddressRouter(privateIpAddress),
+                VPN_INTERFACE_NETMASK,
+                socksServerAddress,
+                udpgwServerAddress,
+                true);
+        Log.addEntry("tun2socks started");
+
+        // Note: should now double-check tunnel routing; see:
+        // https://bitbucket.org/psiphon/psiphon-circumvention-system/src/1dc5e4257dca99790109f3bf374e8ab3a0ead4d7/Android/PsiphonAndroidLibrary/src/com/psiphon3/psiphonlibrary/TunnelCore.java?at=default#cl-779
+    }
+
+    private ParcelFileDescriptor establishVpn(String privateIpAddress)
+        throws Utils.PsibotError {
+
+        Locale previousLocale = Locale.getDefault();
+        ParcelFileDescriptor vpnInterfaceFileDescriptor = null;
+
+        try {
+            String subnet = Utils.getPrivateAddressSubnet(privateIpAddress);
+            int prefixLength = Utils.getPrivateAddressPrefixLength(privateIpAddress);
+            String router = Utils.getPrivateAddressRouter(privateIpAddress);
+
+            // Set the locale to English (or probably any other language that
+            // uses Hindu-Arabic (aka Latin) numerals).
+            // We have found that VpnService.Builder does something locale-dependent
+            // internally that causes errors when the locale uses its own numerals
+            // (i.e., Farsi and Arabic).
+            Locale.setDefault(new Locale("en"));
+
+            vpnInterfaceFileDescriptor = new VpnService.Builder()
+                    .setSession(getString(R.string.app_name))
+                    .setMtu(VPN_INTERFACE_MTU)
+                    .addAddress(privateIpAddress, prefixLength)
+                    .addRoute("0.0.0.0", 0)
+                    .addRoute(subnet, prefixLength)
+                    .addDnsServer(router)
+                    .establish();
+
+            if (vpnInterfaceFileDescriptor == null) {
+                // as per http://developer.android.com/reference/android/net/VpnService.Builder.html#establish%28%29
+                throw new Utils.PsibotError("application is not prepared or is revoked");
+            }
+        } catch(IllegalArgumentException e) {
+            throw new Utils.PsibotError(e);
+        } catch(IllegalStateException e) {
+            throw new Utils.PsibotError(e);
+        } catch(SecurityException e) {
+            throw new Utils.PsibotError(e);
+        } finally {
+            // Restore the original locale.
+            Locale.setDefault(previousLocale);
+        }
+
+        return vpnInterfaceFileDescriptor;
+    }
+
+    private void stopVpn() {
+        // Tun2socks closes the VPN file descriptor, which closes the VpnService session
+        Tun2Socks.stop();
+        Log.addEntry("VPN stopped");
+    }
+
+    private Notification makeForegroundNotification() {
+        Intent intent = new Intent(this, MainActivity.class);
+        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+        intent.setAction("android.intent.action.MAIN");
+        PendingIntent pendingIntent =
+                PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
+
+        Notification.Builder notificationBuilder =
+                new Notification.Builder(this)
+                        .setContentIntent(pendingIntent)
+                        .setContentTitle(getString(R.string.foreground_service_notification_content_title))
+                        .setSmallIcon(R.drawable.ic_launcher);
+
+        return notificationBuilder.build();
+    }
+}

+ 122 - 0
AndroidApp/app/src/main/java/ca/psiphon/psibot/SocketProtector.java

@@ -0,0 +1,122 @@
+/*
+ * Copyright (c) 2014, Psiphon Inc.
+ * All rights reserved.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package ca.psiphon.psibot;
+
+import android.net.LocalServerSocket;
+import android.net.LocalSocket;
+import android.net.VpnService;
+import android.os.ParcelFileDescriptor;
+
+import java.io.FileDescriptor;
+import java.io.IOException;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+
+public class SocketProtector {
+
+    final private VpnService mVpnService;
+    private LocalServerSocket mLocalServerSocket;
+    private Thread mThread;
+
+    // Note: must match value in app\src\main\res\raw\psiphon.config
+    public static final String SOCKET_PROTECTOR_ADDRESS = "/psibot/socketProtector";
+
+    public SocketProtector(VpnService vpnService) {
+        mVpnService = vpnService;
+    }
+
+    public void start() throws Utils.PsibotError {
+        stop();
+        try {
+            mLocalServerSocket = new LocalServerSocket(SOCKET_PROTECTOR_ADDRESS);
+        } catch (IOException e) {
+            throw new Utils.PsibotError("failed to start socket protector", e);
+        }
+        mThread = new Thread(new Runnable() {
+            @Override
+            public void run() {
+                String stoppingMessage = "socket protector stopping";
+                try {
+                    LocalSocket socket = mLocalServerSocket.accept();
+                    // TODO: need to do a read()?
+                    for (FileDescriptor fileDescriptor : socket.getAncillaryFileDescriptors()) {
+                        protectSocket(fileDescriptor);
+                    }
+                } catch (Utils.PsibotError e) {
+                    stoppingMessage += ": " + e.getMessage();
+                } catch (IOException e) {
+                    stoppingMessage += ": " + e.getMessage();
+                }
+                Log.addEntry(stoppingMessage);
+            }
+        });
+        mThread.start();
+        Log.addEntry("socket protector started");
+    }
+
+    public void stop() {
+        if (mLocalServerSocket != null) {
+            try {
+                mLocalServerSocket.close();
+            } catch (IOException e) {
+            }
+        }
+        if (mThread != null) {
+            try {
+                mThread.join();
+            } catch (InterruptedException e) {
+                Thread.currentThread().interrupt();
+            }
+        }
+        mLocalServerSocket = null;
+        mThread = null;
+    }
+
+    private void protectSocket(FileDescriptor fileDescriptor) throws Utils.PsibotError {
+        // Based on this code:
+        // https://code.google.com/p/ics-openvpn/source/browse/main/src/main/java/de/blinkt/openvpn/core/OpenVpnManagementThread.java#164
+        /*
+         * Copyright (c) 2012-2014 Arne Schwabe
+         * Distributed under the GNU GPL v2. For full terms see the file doc/LICENSE.txt
+         */
+        final String errorMessage = "failed to protect socket";
+        try {
+            Method getInt = FileDescriptor.class.getDeclaredMethod("getInt$");
+            int fd = (Integer) getInt.invoke(fileDescriptor);
+            if (!mVpnService.protect(fd)) {
+                throw new Utils.PsibotError(errorMessage);
+            }
+            ParcelFileDescriptor.fromFd(fd).close();
+            // TODO: NativeUtils.jniclose(fdint); ...?
+        } catch (NoSuchMethodException e) {
+            throw new Utils.PsibotError(errorMessage, e);
+        } catch (IllegalArgumentException e) {
+            throw new Utils.PsibotError(errorMessage, e);
+        } catch (IllegalAccessException e) {
+            throw new Utils.PsibotError(errorMessage, e);
+        } catch (InvocationTargetException e) {
+            throw new Utils.PsibotError(errorMessage, e);
+        } catch (NullPointerException e) {
+            throw new Utils.PsibotError(errorMessage, e);
+        } catch (IOException e) {
+            throw new Utils.PsibotError(errorMessage, e);
+        }
+    }
+}

+ 102 - 0
AndroidApp/app/src/main/java/ca/psiphon/psibot/Tun2Socks.java

@@ -0,0 +1,102 @@
+/*
+ * Copyright (c) 2014, Psiphon Inc.
+ * All rights reserved.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package ca.psiphon.psibot;
+
+import android.content.Context;
+import android.content.Intent;
+import android.os.ParcelFileDescriptor;
+
+import java.util.concurrent.atomic.AtomicBoolean;
+
+public class Tun2Socks {
+
+    // Note: can't run more than one tun2socks instance due to the use of
+    // global state (the lwip module, etc.) in the native code.
+
+    // Note: assumes only one instance of Tun2Socks
+    private static Thread mThread;
+    private static AtomicBoolean mIsRunning = new AtomicBoolean();
+
+    public static synchronized void start(
+            final Context context,
+            final ParcelFileDescriptor vpnInterfaceFileDescriptor,
+            final int vpnInterfaceMTU,
+            final String vpnIpAddress,
+            final String vpnNetMask,
+            final String socksServerAddress,
+            final String udpgwServerAddress,
+            final boolean udpgwTransparentDNS) {
+        stop();
+        mThread = new Thread(new Runnable() {
+            @Override
+            public void run() {
+                mIsRunning.set(true);
+                runTun2Socks(
+                        vpnInterfaceFileDescriptor.detachFd(),
+                        vpnInterfaceMTU,
+                        vpnIpAddress,
+                        vpnNetMask,
+                        socksServerAddress,
+                        udpgwServerAddress,
+                        udpgwTransparentDNS ? 1 : 0);
+            	
+                if (!mIsRunning.get()) {
+                    Log.addEntry("Tun2Socks: unexpected termination");
+                    context.stopService(new Intent(context, Service.class));
+                }
+            }
+        });
+        mThread.start();
+    }
+    
+    public static synchronized void stop() {
+        if (mThread != null) {
+            terminateTun2Socks();
+            try {
+                mThread.join();
+            } catch (InterruptedException e) {
+                Thread.currentThread().interrupt();
+            }
+            mThread = null;
+            mIsRunning.set(false);
+        }
+    }
+        
+    public static void logTun2Socks(String level, String channel, String msg) {
+        String logMsg = "Tun2Socks: " + level + "(" + channel + "): " + msg;
+        Log.addEntry(logMsg);
+    }
+
+    private native static int runTun2Socks(
+            int vpnInterfaceFileDescriptor,
+            int vpnInterfaceMTU,
+            String vpnIpAddress,
+            String vpnNetMask,
+            String socksServerAddress,
+            String udpgwServerAddress,
+            int udpgwTransparentDNS);
+
+    private native static void terminateTun2Socks();
+    
+    static
+    {
+        System.loadLibrary("tun2socks");
+    }
+}

+ 208 - 0
AndroidApp/app/src/main/java/ca/psiphon/psibot/Utils.java

@@ -0,0 +1,208 @@
+/*
+ * Copyright (c) 2014, Psiphon Inc.
+ * All rights reserved.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package ca.psiphon.psibot;
+
+import android.content.Context;
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
+
+import org.apache.http.conn.util.InetAddressUtils;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.InetAddress;
+import java.net.NetworkInterface;
+import java.net.SocketException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.zip.ZipInputStream;
+
+
+public class Utils {
+
+    public static class PsibotError extends Exception {
+        private static final long serialVersionUID = 1L;
+
+        public PsibotError() {
+            super();
+        }
+
+        public PsibotError(String message) {
+            super(message);
+        }
+
+        public PsibotError(String message, Throwable cause) {
+            super(message, cause);
+        }
+
+        public PsibotError(Throwable cause) {
+            super(cause);
+        }
+    }
+
+    public static void copyStream(
+            InputStream inputStream, OutputStream outputStream) throws IOException {
+        try {
+            byte[] buffer = new byte[16384];
+            int length;
+            while ((length = inputStream.read(buffer)) != -1) {
+                outputStream.write(buffer, 0 , length);
+            }
+        } finally {
+            inputStream.close();
+            outputStream.close();
+        }
+    }
+
+    public static void writeRawResourceFile(
+            Context context, int resId, File file, boolean setExecutable) throws IOException {
+        file.delete();
+        InputStream zippedAsset = context.getResources().openRawResource(resId);
+        ZipInputStream zipStream = new ZipInputStream(zippedAsset);
+        zipStream.getNextEntry();
+        Utils.copyStream(zipStream, new FileOutputStream(file));
+        if (setExecutable && !file.setExecutable(true)) {
+            throw new IOException("failed to set file as executable");
+        }
+    }
+
+    public static String getNetworkTypeName(Context context) {
+        ConnectivityManager connectivityManager =
+                (ConnectivityManager)context.getSystemService(Context.CONNECTIVITY_SERVICE);
+        NetworkInfo networkInfo = connectivityManager.getActiveNetworkInfo();
+        return networkInfo == null ? "" : networkInfo.getTypeName();
+    }
+
+    private static final String CANDIDATE_10_SLASH_8 = "10.0.0.1";
+    private static final String SUBNET_10_SLASH_8 = "10.0.0.0";
+    private static final int PREFIX_LENGTH_10_SLASH_8 = 8;
+    private static final String ROUTER_10_SLASH_8 = "10.0.0.2";
+
+    private static final String CANDIDATE_172_16_SLASH_12 = "172.16.0.1";
+    private static final String SUBNET_172_16_SLASH_12 = "172.16.0.0";
+    private static final int PREFIX_LENGTH_172_16_SLASH_12 = 12;
+    private static final String ROUTER_172_16_SLASH_12 = "172.16.0.2";
+
+    private static final String CANDIDATE_192_168_SLASH_16 = "192.168.0.1";        
+    private static final String SUBNET_192_168_SLASH_16 = "192.168.0.0";
+    private static final int PREFIX_LENGTH_192_168_SLASH_16 = 16;
+    private static final String ROUTER_192_168_SLASH_16 = "192.168.0.2";
+    
+    private static final String CANDIDATE_169_254_1_SLASH_24 = "169.254.1.1";        
+    private static final String SUBNET_169_254_1_SLASH_24 = "169.254.1.0";
+    private static final int PREFIX_LENGTH_169_254_1_SLASH_24 = 24;
+    private static final String ROUTER_169_254_1_SLASH_24 = "169.254.1.2";
+    
+    public static String selectPrivateAddress() {
+        // Select one of 10.0.0.1, 172.16.0.1, or 192.168.0.1 depending on
+        // which private address range isn't in use.
+
+        ArrayList<String> candidates = new ArrayList<String>();
+        candidates.add(CANDIDATE_10_SLASH_8);
+        candidates.add(CANDIDATE_172_16_SLASH_12);
+        candidates.add(CANDIDATE_192_168_SLASH_16);
+        candidates.add(CANDIDATE_169_254_1_SLASH_24);
+        
+        List<NetworkInterface> netInterfaces;
+        try {
+            netInterfaces = Collections.list(NetworkInterface.getNetworkInterfaces());
+        } catch (SocketException e) {
+            return null;
+        }
+
+        for (NetworkInterface netInterface : netInterfaces) {
+            for (InetAddress inetAddress : Collections.list(netInterface.getInetAddresses())) {
+                String ipAddress = inetAddress.getHostAddress();
+                if (InetAddressUtils.isIPv4Address(ipAddress)) {
+                    if (ipAddress.startsWith("10.")) {
+                        candidates.remove(CANDIDATE_10_SLASH_8);
+                    }
+                    else if (
+                        ipAddress.length() >= 6 &&
+                        ipAddress.substring(0, 6).compareTo("172.16") >= 0 && 
+                        ipAddress.substring(0, 6).compareTo("172.31") <= 0) {
+                        candidates.remove(CANDIDATE_172_16_SLASH_12);
+                    }
+                    else if (ipAddress.startsWith("192.168")) {
+                        candidates.remove(CANDIDATE_192_168_SLASH_16);
+                    }
+                }
+            }
+        }
+        
+        if (candidates.size() > 0) {
+            return candidates.get(0);
+        }
+        
+        return null;
+    }
+    
+    public static String getPrivateAddressSubnet(String privateIpAddress) {
+        if (0 == privateIpAddress.compareTo(CANDIDATE_10_SLASH_8)) {
+            return SUBNET_10_SLASH_8;
+        }
+        else if (0 == privateIpAddress.compareTo(CANDIDATE_172_16_SLASH_12)) {
+            return SUBNET_172_16_SLASH_12;
+        }
+        else if (0 == privateIpAddress.compareTo(CANDIDATE_192_168_SLASH_16)) {
+            return SUBNET_192_168_SLASH_16;
+        }
+        else if (0 == privateIpAddress.compareTo(CANDIDATE_169_254_1_SLASH_24)) {
+            return SUBNET_169_254_1_SLASH_24;
+        }
+        return null;
+    }
+    
+    public static int getPrivateAddressPrefixLength(String privateIpAddress) {
+        if (0 == privateIpAddress.compareTo(CANDIDATE_10_SLASH_8)) {
+            return PREFIX_LENGTH_10_SLASH_8;
+        }
+        else if (0 == privateIpAddress.compareTo(CANDIDATE_172_16_SLASH_12)) {
+            return PREFIX_LENGTH_172_16_SLASH_12;
+        }
+        else if (0 == privateIpAddress.compareTo(CANDIDATE_192_168_SLASH_16)) {
+            return PREFIX_LENGTH_192_168_SLASH_16;
+        }
+        else if (0 == privateIpAddress.compareTo(CANDIDATE_169_254_1_SLASH_24)) {
+            return PREFIX_LENGTH_169_254_1_SLASH_24;
+        }
+        return 0;
+    }
+    
+    public static String getPrivateAddressRouter(String privateIpAddress) {
+        if (0 == privateIpAddress.compareTo(CANDIDATE_10_SLASH_8)) {
+            return ROUTER_10_SLASH_8;
+        }
+        else if (0 == privateIpAddress.compareTo(CANDIDATE_172_16_SLASH_12)) {
+            return ROUTER_172_16_SLASH_12;
+        }
+        else if (0 == privateIpAddress.compareTo(CANDIDATE_192_168_SLASH_16)) {
+            return ROUTER_192_168_SLASH_16;
+        }
+        else if (0 == privateIpAddress.compareTo(CANDIDATE_169_254_1_SLASH_24)) {
+            return ROUTER_169_254_1_SLASH_24;
+        }
+        return null;
+    }
+}

BIN
AndroidApp/app/src/main/res/drawable-hdpi/ic_launcher.png


BIN
AndroidApp/app/src/main/res/drawable-mdpi/ic_launcher.png


BIN
AndroidApp/app/src/main/res/drawable-xhdpi/ic_launcher.png


BIN
AndroidApp/app/src/main/res/drawable-xxhdpi/ic_launcher.png


+ 7 - 0
AndroidApp/app/src/main/res/layout/activity_main.xml

@@ -0,0 +1,7 @@
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:id="@+id/container"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    tools:context=".MainActivity"
+    tools:ignore="MergeRootFrame" />

+ 26 - 0
AndroidApp/app/src/main/res/layout/log_entry_row.xml

@@ -0,0 +1,26 @@
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="fill_parent"
+    android:layout_height="wrap_content"
+    android:orientation="horizontal"
+    android:padding="4dp" >
+ 
+    <TextView
+        android:id="@+id/log_timestamp_text"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:typeface="sans"
+        android:textSize="12sp"
+        android:textColor="#808080"
+        android:layout_marginRight="6dp"
+        android:layout_marginEnd="6dp"/>
+ 
+    <TextView
+        android:id="@+id/log_message_text"
+        android:layout_width="fill_parent"
+        android:layout_height="wrap_content"
+        android:layout_toRightOf="@id/log_timestamp_text"
+        android:layout_toEndOf="@id/log_timestamp_text"
+        android:typeface="sans"
+        android:textSize="12sp"/>
+
+</RelativeLayout>

+ 8 - 0
AndroidApp/app/src/main/res/menu/main.xml

@@ -0,0 +1,8 @@
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    tools:context=".MainActivity" >
+    <item android:id="@+id/action_email_log"
+        android:title="@string/action_email_log"
+        android:orderInCategory="100"
+        android:showAsAction="never" />
+</menu>

+ 12 - 0
AndroidApp/app/src/main/res/raw/psiphon_config

@@ -0,0 +1,12 @@
+{
+    "PropagationChannelId" : "<placeholder>",
+    "SponsorId" : "<placeholder>",
+    "RemoteServerListUrl" : "<placeholder>",
+    "RemoteServerListSignaturePublicKey" : "<placeholder>",
+    "LogFilename" : "",
+    "LocalHttpProxyPort" : 0,
+    "LocalSocksProxyPort" : 0,
+    "EgressRegion" : "",
+    "TunnelProtocol" : "",
+    "ConnectionWorkerPoolSize" : 10
+}

BIN
AndroidApp/app/src/main/res/raw/psiphon_tunnel_core_arm


+ 5 - 0
AndroidApp/app/src/main/res/values-v21/styles.xml

@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <style name="AppTheme" parent="android:Theme.Material.Light">
+    </style>
+</resources>

+ 6 - 0
AndroidApp/app/src/main/res/values-w820dp/dimens.xml

@@ -0,0 +1,6 @@
+<resources>
+    <!-- Example customization of dimensions originally defined in res/values/dimens.xml
+         (such as screen margins) for screens with more than 820dp of available width. This
+         would include 7" and 10" devices in landscape (~960dp and ~1280dp respectively). -->
+    <dimen name="activity_horizontal_margin">64dp</dimen>
+</resources>

+ 5 - 0
AndroidApp/app/src/main/res/values/dimens.xml

@@ -0,0 +1,5 @@
+<resources>
+    <!-- Default screen margins, per the Android Design guidelines. -->
+    <dimen name="activity_horizontal_margin">16dp</dimen>
+    <dimen name="activity_vertical_margin">16dp</dimen>
+</resources>

+ 8 - 0
AndroidApp/app/src/main/res/values/strings.xml

@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+    <string name="app_name">Psibot</string>
+    <string name="foreground_service_notification_content_title">Psibot is running</string>
+    <string name="action_email_log">Email Log</string>
+
+</resources>

+ 8 - 0
AndroidApp/app/src/main/res/values/styles.xml

@@ -0,0 +1,8 @@
+<resources>
+
+    <!-- Base application theme. -->
+    <style name="AppTheme" parent="android:Theme.Holo.Light.DarkActionBar">
+        <!-- Customize your theme here. -->
+    </style>
+
+</resources>

+ 4 - 0
AndroidApp/app/src/main/res/values/symbols.xml

@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <string name="foregroundServiceNotificationId">psibotService</string>
+</resources>

+ 19 - 0
AndroidApp/build.gradle

@@ -0,0 +1,19 @@
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+
+buildscript {
+    repositories {
+        jcenter()
+    }
+    dependencies {
+        classpath 'com.android.tools.build:gradle:0.12.2'
+
+        // NOTE: Do not place your application dependencies here; they belong
+        // in the individual module build.gradle files
+    }
+}
+
+allprojects {
+    repositories {
+        jcenter()
+    }
+}

+ 18 - 0
AndroidApp/gradle.properties

@@ -0,0 +1,18 @@
+# Project-wide Gradle settings.
+
+# IDE (e.g. Android Studio) users:
+# Settings specified in this file will override any Gradle settings
+# configured through the IDE.
+
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+# Default value: -Xmx10248m -XX:MaxPermSize=256m
+# org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
+
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. More details, visit
+# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
+# org.gradle.parallel=true

BIN
AndroidApp/gradle/wrapper/gradle-wrapper.jar


+ 6 - 0
AndroidApp/gradle/wrapper/gradle-wrapper.properties

@@ -0,0 +1,6 @@
+#Wed Apr 10 15:27:10 PDT 2013
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=http\://services.gradle.org/distributions/gradle-1.12-all.zip

+ 164 - 0
AndroidApp/gradlew

@@ -0,0 +1,164 @@
+#!/usr/bin/env bash
+
+##############################################################################
+##
+##  Gradle start up script for UN*X
+##
+##############################################################################
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS=""
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn ( ) {
+    echo "$*"
+}
+
+die ( ) {
+    echo
+    echo "$*"
+    echo
+    exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+case "`uname`" in
+  CYGWIN* )
+    cygwin=true
+    ;;
+  Darwin* )
+    darwin=true
+    ;;
+  MINGW* )
+    msys=true
+    ;;
+esac
+
+# For Cygwin, ensure paths are in UNIX format before anything is touched.
+if $cygwin ; then
+    [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
+fi
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+    ls=`ls -ld "$PRG"`
+    link=`expr "$ls" : '.*-> \(.*\)$'`
+    if expr "$link" : '/.*' > /dev/null; then
+        PRG="$link"
+    else
+        PRG=`dirname "$PRG"`"/$link"
+    fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >&-
+APP_HOME="`pwd -P`"
+cd "$SAVED" >&-
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+    if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+        # IBM's JDK on AIX uses strange locations for the executables
+        JAVACMD="$JAVA_HOME/jre/sh/java"
+    else
+        JAVACMD="$JAVA_HOME/bin/java"
+    fi
+    if [ ! -x "$JAVACMD" ] ; then
+        die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+    fi
+else
+    JAVACMD="java"
+    which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
+    MAX_FD_LIMIT=`ulimit -H -n`
+    if [ $? -eq 0 ] ; then
+        if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+            MAX_FD="$MAX_FD_LIMIT"
+        fi
+        ulimit -n $MAX_FD
+        if [ $? -ne 0 ] ; then
+            warn "Could not set maximum file descriptor limit: $MAX_FD"
+        fi
+    else
+        warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+    fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+    GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin ; then
+    APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+    CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+
+    # We build the pattern for arguments to be converted via cygpath
+    ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+    SEP=""
+    for dir in $ROOTDIRSRAW ; do
+        ROOTDIRS="$ROOTDIRS$SEP$dir"
+        SEP="|"
+    done
+    OURCYGPATTERN="(^($ROOTDIRS))"
+    # Add a user-defined pattern to the cygpath arguments
+    if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+        OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+    fi
+    # Now convert the arguments - kludge to limit ourselves to /bin/sh
+    i=0
+    for arg in "$@" ; do
+        CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+        CHECK2=`echo "$arg"|egrep -c "^-"`                                 ### Determine if an option
+
+        if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then                    ### Added a condition
+            eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+        else
+            eval `echo args$i`="\"$arg\""
+        fi
+        i=$((i+1))
+    done
+    case $i in
+        (0) set -- ;;
+        (1) set -- "$args0" ;;
+        (2) set -- "$args0" "$args1" ;;
+        (3) set -- "$args0" "$args1" "$args2" ;;
+        (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+        (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+        (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+        (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+        (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+        (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+    esac
+fi
+
+# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
+function splitJvmOpts() {
+    JVM_OPTS=("$@")
+}
+eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
+JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
+
+exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"

+ 90 - 0
AndroidApp/gradlew.bat

@@ -0,0 +1,90 @@
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem  Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS=
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto init
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto init
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:init
+@rem Get command-line arguments, handling Windowz variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_args
+if "%@eval[2+2]" == "4" goto 4NT_args
+
+:win9xME_args
+@rem Slurp the command line arguments.
+set CMD_LINE_ARGS=
+set _SKIP=2
+
+:win9xME_args_slurp
+if "x%~1" == "x" goto execute
+
+set CMD_LINE_ARGS=%*
+goto execute
+
+:4NT_args
+@rem Get arguments from the 4NT Shell from JP Software
+set CMD_LINE_ARGS=%$
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if  not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega

+ 1 - 0
AndroidApp/settings.gradle

@@ -0,0 +1 @@
+include ':app'