Commits

rhynes committed 6d73049

Initial implementation of web app wrapper.

Comments (0)

Files changed (24)

Android/PsiphonAndroidLibrary/src/com/psiphon3/psiphonlibrary/TunnelCore.java

         CONNECTING,
         CONNECTED
     }
+    private Context m_parentContext = null;
     private Service m_parentService = null;
     private State m_state = State.DISCONNECTED;
     private boolean m_firstStart = true;
         public void stop();
     }
     
-    TunnelCore(Service parentService)
+    public TunnelCore(Context parentContext, Service parentService)
     {
+        m_parentContext = parentContext;
         m_parentService = parentService;
     }
     
     public void onCreate()
     {
         MyLog.logger = this;
-        m_interface = new ServerInterface(m_parentService);
-        m_serverSelector = new ServerSelector(m_interface, m_parentService);
+        m_interface = new ServerInterface(m_parentContext);
+        m_serverSelector = new ServerSelector(m_interface, m_parentContext);
     }
 
     // Implementation of android.app.Service.onDestroy
 
     private void doForeground()
     {
+        if (m_parentService == null)
+        {
+            // Only works with a Service
+            return;
+        }
+
         m_parentService.startForeground(R.string.psiphon_service_notification_id, this.createNotification());
     }
     
     private Notification createNotification()
     {
+        if (m_parentService == null)
+        {
+            // Only works with a Service
+            return null;
+        }
+
         int contentTextID = -1;
         int iconID = -1;
         
     {
         if (m_eventsInterface != null)
         {
-            m_eventsInterface.appendStatusMessage(m_parentService, message, messageClass);
+            m_eventsInterface.appendStatusMessage(m_parentContext, message, messageClass);
         }
     }
         
     {
         m_state = newState;
         
-        if (!this.m_destroyed)
+        if (!this.m_destroyed && m_parentService != null)
         {
             String ns = Context.NOTIFICATION_SERVICE;
             NotificationManager mNotificationManager =
     
                 try
                 {
-                    TransparentProxyConfig.setupTransparentProxyRouting(m_parentService);
+                    TransparentProxyConfig.setupTransparentProxyRouting(m_parentContext);
                     cleanupTransparentProxyRouting = true;
                 }
                 catch (PsiphonTransparentProxyException e)
 
                 if (m_eventsInterface != null)
                 {
-                    m_eventsInterface.signalHandshakeSuccess(m_parentService);
+                    m_eventsInterface.signalHandshakeSuccess(m_parentContext);
                 }
             } 
             catch (PsiphonServerInterfaceException requestException)
             {
                 try
                 {
-                    TransparentProxyConfig.teardownTransparentProxyRouting(m_parentService);
+                    TransparentProxyConfig.teardownTransparentProxyRouting(m_parentContext);
                 }
                 catch (PsiphonTransparentProxyException e)
                 {
                 
                 if (m_eventsInterface != null)
                 {
-                    m_eventsInterface.signalUnexpectedDisconnect(m_parentService);
+                    m_eventsInterface.signalUnexpectedDisconnect(m_parentContext);
                 }
             }
             
     @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
     private void doVpnProtect(Socket socket)
     {
+        // *Must* have a parent service for this mode
+        assert (m_parentService != null);
+
         ((TunnelVpnService)m_parentService).protect(socket);
     }
     
     @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
     private ParcelFileDescriptor doVpnBuilder(String tunnelWholeDeviceDNSServer)
     {
+        // *Must* have a parent service for this mode
+        assert (m_parentService != null);
+
         ParcelFileDescriptor vpnInterfaceFileDescriptor = null;
         String builderErrorMessage = null;
         try
 
         if (m_eventsInterface != null)
         {
-            m_eventsInterface.signalTunnelStarting(m_parentService);
+            m_eventsInterface.signalTunnelStarting(m_parentContext);
         }
 
         MyLog.v(R.string.starting_tunnel, MyLog.Sensitivity.NOT_SENSITIVE);
     
     public void stopVpnServiceHelper()
     {
-    	// A hack to stop the VpnService, which doesn't respond to normal
-    	// stopService() calls.
+        // *Must* have a parent service for this mode
+        assert (m_parentService != null);
+
+        // A hack to stop the VpnService, which doesn't respond to normal
+        // stopService() calls.
 
         // Stopping tun2socks will close the VPN interface fd, which
         // in turn stops the VpnService. Without closing the fd, the
         {
             if (m_eventsInterface != null)
             {
-                m_eventsInterface.signalTunnelStopping(m_parentService);
+                m_eventsInterface.signalTunnelStopping(m_parentContext);
             }
 
             // TODO: ServerListReorder lifetime on Android isn't the same as on Windows

Android/PsiphonAndroidLibrary/src/com/psiphon3/psiphonlibrary/TunnelService.java

 
 public class TunnelService extends Service
 {
-    private TunnelCore m_Core = new TunnelCore(this);
+    private TunnelCore m_Core = new TunnelCore(this, this);
 
     public class LocalBinder extends Binder
     {

Android/PsiphonAndroidLibrary/src/com/psiphon3/psiphonlibrary/TunnelVpnService.java

 @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
 public class TunnelVpnService extends VpnService
 {
-    private TunnelCore m_Core = new TunnelCore(this);
+    private TunnelCore m_Core = new TunnelCore(this, this);
 
     public class LocalBinder extends Binder
     {
     @Override
     public IBinder onBind(Intent intent)
     {
-    	// Need to use super class behavior in specified cases:
-    	// http://developer.android.com/reference/android/net/VpnService.html#onBind%28android.content.Intent%29
-    	
-    	String action = intent.getAction();
-    	if (action != null && action.equals(SERVICE_INTERFACE))
-    	{
-        	return super.onBind(intent);
-    	}
-    	
-		return m_binder;
+        // Need to use super class behavior in specified cases:
+        // http://developer.android.com/reference/android/net/VpnService.html#onBind%28android.content.Intent%29
+        
+        String action = intent.getAction();
+        if (action != null && action.equals(SERVICE_INTERFACE))
+        {
+            return super.onBind(intent);
+        }
+        
+        return m_binder;
     }
 
     @Override
     @Override
     public void onCreate()
     {
-    	PsiphonData.getPsiphonData().setCurrentTunnelCore(m_Core);
+        PsiphonData.getPsiphonData().setCurrentTunnelCore(m_Core);
         m_Core.onCreate();
     }
 
     @Override
     public void onDestroy()
     {
-    	PsiphonData.getPsiphonData().setCurrentTunnelCore(null);
+        PsiphonData.getPsiphonData().setCurrentTunnelCore(null);
         m_Core.onDestroy();
     }
 

Android/PsiphonProxiedWebApp/.classpath

+<?xml version="1.0" encoding="UTF-8"?>
+<classpath>
+	<classpathentry kind="src" path="src"/>
+	<classpathentry kind="src" path="gen"/>
+	<classpathentry kind="con" path="com.android.ide.eclipse.adt.ANDROID_FRAMEWORK"/>
+	<classpathentry kind="con" path="com.android.ide.eclipse.adt.LIBRARIES"/>
+	<classpathentry kind="src" path="/PsiphonAndroidLibrary"/>
+	<classpathentry kind="output" path="bin/classes"/>
+</classpath>

Android/PsiphonProxiedWebApp/.project

+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+	<name>PsiphonProxiedWebApp</name>
+	<comment></comment>
+	<projects>
+	</projects>
+	<buildSpec>
+		<buildCommand>
+			<name>com.android.ide.eclipse.adt.ResourceManagerBuilder</name>
+			<arguments>
+			</arguments>
+		</buildCommand>
+		<buildCommand>
+			<name>com.android.ide.eclipse.adt.PreCompilerBuilder</name>
+			<arguments>
+			</arguments>
+		</buildCommand>
+		<buildCommand>
+			<name>org.eclipse.jdt.core.javabuilder</name>
+			<arguments>
+			</arguments>
+		</buildCommand>
+		<buildCommand>
+			<name>com.android.ide.eclipse.adt.ApkBuilder</name>
+			<arguments>
+			</arguments>
+		</buildCommand>
+	</buildSpec>
+	<natures>
+		<nature>com.android.ide.eclipse.adt.AndroidNature</nature>
+		<nature>org.eclipse.jdt.core.javanature</nature>
+	</natures>
+</projectDescription>

Android/PsiphonProxiedWebApp/AndroidManifest.xml

+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.psiphon3.psiphonproxiedwebapp"
+    android:versionCode="1"
+    android:versionName="1.0" >
+
+    <uses-sdk
+        android:minSdkVersion="8"
+        android:targetSdkVersion="14" />
+        
+    <uses-permission android:name="android.permission.INTERNET"></uses-permission>    
+    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"></uses-permission>
+    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"></uses-permission>   
+
+    <application
+        android:allowBackup="true"
+        android:icon="@drawable/ic_launcher"
+        android:label="@string/app_name"
+        android:theme="@style/AppTheme" >
+        <activity
+            android:name="com.psiphon3.psiphonproxiedwebapp.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>
+    </application>
+
+</manifest>

Android/PsiphonProxiedWebApp/ant.properties

+# This file is used to override default values used by the Ant build system.
+#
+# This file must be checked into Version Control Systems, as it is
+# integral to the build system of your project.
+
+# This file is only used by the Ant script.
+
+# You can use this to override default values such as
+#  'source.dir' for the location of your java source folder and
+#  'out.dir' for the location of your output folder.
+
+# You can also use it define how the release builds are signed by declaring
+# the following properties:
+#  'key.store' for the location of your keystore and
+#  'key.alias' for the name of the key to use.
+# The password will be asked during the build when you use the 'release' target.
+

Android/PsiphonProxiedWebApp/build.xml

+<?xml version="1.0" encoding="UTF-8"?>
+<project name="PsiphonProxiedWebApp" default="help">
+
+    <!-- The local.properties file is created and updated by the 'android' tool.
+         It contains the path to the SDK. It should *NOT* be checked into
+         Version Control Systems. -->
+    <property file="local.properties" />
+
+    <!-- The ant.properties file can be created by you. It is only edited by the
+         'android' tool to add properties to it.
+         This is the place to change some Ant specific build properties.
+         Here are some properties you may want to change/update:
+
+         source.dir
+             The name of the source directory. Default is 'src'.
+         out.dir
+             The name of the output directory. Default is 'bin'.
+
+         For other overridable properties, look at the beginning of the rules
+         files in the SDK, at tools/ant/build.xml
+
+         Properties related to the SDK location or the project target should
+         be updated using the 'android' tool with the 'update' action.
+
+         This file is an integral part of the build system for your
+         application and should be checked into Version Control Systems.
+
+         -->
+    <property file="ant.properties" />
+
+    <!-- The project.properties file is created and updated by the 'android'
+         tool, as well as ADT.
+
+         This contains project specific properties such as project target, and library
+         dependencies. Lower level build properties are stored in ant.properties
+         (or in .classpath for Eclipse projects).
+
+         This file is an integral part of the build system for your
+         application and should be checked into Version Control Systems. -->
+    <loadproperties srcFile="project.properties" />
+
+    <!-- quick check on sdk.dir -->
+    <fail
+            message="sdk.dir is missing. Make sure to generate local.properties using 'android update project' or to inject it through an env var"
+            unless="sdk.dir"
+    />
+
+    <!--
+        Import per project custom build rules if present at the root of the project.
+        This is the place to put custom intermediary targets such as:
+            -pre-build
+            -pre-compile
+            -post-compile (This is typically used for code obfuscation.
+                           Compiled code location: ${out.classes.absolute.dir}
+                           If this is not done in place, override ${out.dex.input.absolute.dir})
+            -post-package
+            -post-build
+            -pre-clean
+    -->
+    <import file="custom_rules.xml" optional="true" />
+
+    <!-- Import the actual build file.
+
+         To customize existing targets, there are two options:
+         - Customize only one target:
+             - copy/paste the target into this file, *before* the
+               <import> task.
+             - customize it to your needs.
+         - Customize the whole content of build.xml
+             - copy/paste the content of the rules files (minus the top node)
+               into this file, replacing the <import> task.
+             - customize to your needs.
+
+         ***********************
+         ****** IMPORTANT ******
+         ***********************
+         In all cases you must update the value of version-tag below to read 'custom' instead of an integer,
+         in order to avoid having your file be overridden by tools such as "android update project"
+    -->
+    <!-- version-tag: 1 -->
+    <import file="${sdk.dir}/tools/ant/build.xml" />
+
+</project>

Android/PsiphonProxiedWebApp/libs/android-support-v4.jar

Binary file added.

Android/PsiphonProxiedWebApp/proguard-project.txt

+# To enable ProGuard in your project, edit project.properties
+# to define the proguard.config property as described in that file.
+#
+# Add project specific ProGuard rules here.
+# By default, the flags in this file are appended to flags specified
+# in ${sdk.dir}/tools/proguard/proguard-android.txt
+# You can edit the include path and order by changing the ProGuard
+# include property in project.properties.
+#
+# 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 *;
+#}

Android/PsiphonProxiedWebApp/project.properties

+# This file is automatically generated by Android Tools.
+# Do not modify this file -- YOUR CHANGES WILL BE ERASED!
+#
+# This file must be checked in Version Control Systems.
+#
+# To customize properties used by the Ant build system edit
+# "ant.properties", and override values to adapt the script to your
+# project structure.
+#
+# To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home):
+#proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt
+
+# Project target.
+target=android-14
+android.library.reference.1=../zirco-browser
+android.library.reference.2=../PsiphonAndroidLibrary

Android/PsiphonProxiedWebApp/res/drawable-hdpi/ic_launcher.png

Added
New image

Android/PsiphonProxiedWebApp/res/drawable-ldpi/ic_launcher.png

Added
New image

Android/PsiphonProxiedWebApp/res/drawable-mdpi/ic_launcher.png

Added
New image

Android/PsiphonProxiedWebApp/res/drawable-xhdpi/ic_launcher.png

Added
New image

Android/PsiphonProxiedWebApp/res/drawable/splash_image.png

Added
New image

Android/PsiphonProxiedWebApp/res/layout/activity_main.xml

+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    tools:context=".MainActivity" >
+
+    <WebView xmlns:android="http://schemas.android.com/apk/res/android"
+        android:id="@+id/webView"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent" >
+    </WebView>
+
+</RelativeLayout>

Android/PsiphonProxiedWebApp/res/layout/splash_screen.xml

+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/splash_screen"
+    android:layout_width="fill_parent"
+    android:layout_height="fill_parent"
+    android:background="#000000"
+    android:orientation="vertical"
+    android:padding="8dp" >
+
+    <ImageView
+        android:id="@+id/splash_screen_image"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_gravity="center"
+        android:layout_marginRight="8dp"
+        android:src="@drawable/splash_image" />
+
+    <TextView
+        android:id="@+id/splash_screen_text"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_gravity="center_horizontal"
+        android:text="Starting..."
+        android:textColor="#FFF" />
+
+</LinearLayout>

Android/PsiphonProxiedWebApp/res/menu/activity_main.xml

+<menu xmlns:android="http://schemas.android.com/apk/res/android" >
+
+    <item
+        android:id="@+id/menu_settings"
+        android:orderInCategory="100"
+        android:showAsAction="never"
+        android:title="@string/menu_settings"/>
+
+</menu>

Android/PsiphonProxiedWebApp/res/values-v11/styles.xml

+<resources>
+
+    <!--
+        Base application theme for API 11+. This theme completely replaces
+        AppBaseTheme from res/values/styles.xml on API 11+ devices.
+    -->
+    <style name="AppBaseTheme" parent="android:Theme.Holo.Light">
+        <!-- API 11 theme customizations can go here. -->
+    </style>
+
+</resources>

Android/PsiphonProxiedWebApp/res/values-v14/styles.xml

+<resources>
+
+    <!--
+        Base application theme for API 14+. This theme completely replaces
+        AppBaseTheme from BOTH res/values/styles.xml and
+        res/values-v11/styles.xml on API 14+ devices.
+    -->
+    <style name="AppBaseTheme" parent="android:Theme.Holo.Light.DarkActionBar">
+        <!-- API 14 theme customizations can go here. -->
+    </style>
+
+</resources>

Android/PsiphonProxiedWebApp/res/values/strings.xml

+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+    <string name="app_name">PsiphonProxiedWebApp</string>
+    <string name="hello_world">Hello world!</string>
+    <string name="menu_settings">Settings</string>
+
+</resources>

Android/PsiphonProxiedWebApp/res/values/styles.xml

+<resources>
+
+    <!--
+        Base application theme, dependent on API level. This theme is replaced
+        by AppBaseTheme from res/values-vXX/styles.xml on newer devices.
+    -->
+    <style name="AppBaseTheme" parent="android:Theme.Light">
+        <!--
+            Theme customizations available in newer API levels can go in
+            res/values-vXX/styles.xml, while customizations related to
+            backward-compatibility can go here.
+        -->
+    </style>
+
+    <!-- Application theme. -->
+    <style name="AppTheme" parent="AppBaseTheme">
+        <!-- All customizations that are NOT specific to a particular API-level can go here. -->
+    </style>
+
+</resources>

Android/PsiphonProxiedWebApp/src/com/psiphon3/psiphonproxiedwebapp/MainActivity.java

+package com.psiphon3.psiphonproxiedwebapp;
+
+// TODO:
+// - put splash toast into common code
+// - detect if Psiphon is running ..?
+// - handle tunnel restarts (show splash screen? retain webview location/state)
+// - move tunnel start/stop to onCreate/onDestroy
+
+import org.zirco.utils.ProxySettings;
+
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.annotation.SuppressLint;
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Bitmap;
+import android.graphics.Color;
+import android.graphics.drawable.BitmapDrawable;
+import android.view.Gravity;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.Window;
+import android.webkit.WebSettings;
+import android.webkit.WebView;
+import android.webkit.WebViewClient;
+import android.widget.ImageView;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import com.psiphon3.psiphonlibrary.PsiphonData;
+import com.psiphon3.psiphonlibrary.TunnelCore;
+import com.psiphon3.psiphonlibrary.Utils.MyLog;
+import com.psiphon3.psiphonlibrary.Events;
+
+public class MainActivity extends Activity implements MyLog.ILogInfoProvider, Events
+{
+    private WebView m_webView;
+    private TextView m_textView;
+    private boolean m_splashScreenCancelled = false;
+    private Handler m_handler = new Handler();
+    private TunnelCore m_tunnelCore;
+    
+    // Infinite toast
+    private void showSplashScreen()
+    {
+        m_splashScreenCancelled = false;
+
+        LayoutInflater inflater = (LayoutInflater)getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+        View layout = inflater.inflate(R.layout.splash_screen, (ViewGroup)findViewById(R.id.splash_screen));
+        m_textView = (TextView)layout.findViewById(R.id.splash_screen_text);
+        
+        // Set background to match (0,0) splash image pixel color
+        ImageView imageView = (ImageView)layout.findViewById(R.id.splash_screen_image);
+        Bitmap bitmap = ((BitmapDrawable)imageView.getDrawable()).getBitmap();
+        int pixel = bitmap.getPixel(0, 0);
+        layout.setBackgroundColor(pixel);
+
+        final Toast splashScreen = new Toast(this);
+        splashScreen.setGravity(Gravity.FILL, 0, 0);
+        splashScreen.setDuration(Toast.LENGTH_SHORT);
+        splashScreen.setView(layout);
+
+        Thread t = new Thread()
+        {
+            public void run()
+            {
+                try
+                {
+                    while (!m_splashScreenCancelled)
+                    {
+                        splashScreen.show();
+                        sleep(1850);
+                    }
+                }
+                catch (Exception e)
+                {
+                }
+                
+                m_textView = null;
+            }
+        };
+        t.start();
+    }
+    
+    private void dismissSplashScreen()
+    {
+        m_splashScreenCancelled = true;
+    }
+    
+    private class CustomWebViewClient extends WebViewClient
+    {
+        @Override
+        public boolean shouldOverrideUrlLoading(WebView view, String url)
+        {
+            // Always open links in the proxied WebView
+            return false;
+        }
+    }
+    
+    @SuppressLint("SetJavaScriptEnabled")
+    @Override
+    protected void onCreate(Bundle savedInstanceState)
+    {
+        super.onCreate(savedInstanceState);
+        
+        MyLog.logInfoProvider = this;
+        
+        requestWindowFeature(Window.FEATURE_NO_TITLE);
+        setContentView(R.layout.activity_main);
+        m_webView = (WebView)findViewById(R.id.webView);
+        m_webView.setWebViewClient(new CustomWebViewClient());
+        WebSettings webSettings = m_webView.getSettings();
+        webSettings.setJavaScriptEnabled(true);
+    }
+
+    @Override
+    public boolean onCreateOptionsMenu(Menu menu)
+    {
+        getMenuInflater().inflate(R.menu.activity_main, menu);
+        return true;
+    }
+
+    @Override
+    protected void onResume()
+    {
+        super.onResume();
+
+        showSplashScreen();
+        
+        m_tunnelCore = new TunnelCore(this, null);
+        m_tunnelCore.setEventsInterface(this);
+        m_tunnelCore.onCreate();
+        m_tunnelCore.startTunnel();
+    }
+        
+    @Override
+    protected void onPause()
+    {
+        super.onPause();
+        
+        m_tunnelCore.stopTunnel();
+        m_tunnelCore.onDestroy();
+        m_tunnelCore = null;
+
+        dismissSplashScreen();        
+    }
+
+    @Override
+    public boolean onKeyDown(int keyCode, KeyEvent event)
+    {
+        if ((keyCode == KeyEvent.KEYCODE_BACK) && m_webView.canGoBack())
+        {
+            m_webView.goBack();
+            return true;
+        }
+
+        return super.onKeyDown(keyCode, event);
+    }
+    
+    @Override
+    public int getAndroidLogPriorityEquivalent(int priority)
+    {
+        // TODO: ?
+        return 0;
+    }
+
+    @Override
+    public String getResourceString(int stringResID, Object[] formatArgs)
+    {
+        if (formatArgs == null || formatArgs.length == 0)
+        {
+            return getString(stringResID);
+        }
+        
+        return getString(stringResID, formatArgs);
+    }
+
+    @Override
+    public String getResourceEntryName(int stringResID)
+    {
+        return getResources().getResourceEntryName(stringResID);
+    }
+
+    @Override
+    public void appendStatusMessage(Context context, String message, int messageClass)
+    {
+        final String finalMessage = message;
+
+        m_handler.post(
+            new Runnable()
+            {
+                @Override
+                public void run()
+                {
+                    if (m_textView != null)
+                    {
+                        m_textView.setText(finalMessage);
+                    }
+                }            
+            });
+    }
+
+    private String getHomePage()
+    {
+        // Only supports one home page
+        for (String homePage : PsiphonData.getPsiphonData().getHomePages())
+        {
+            return homePage;
+        }
+
+        return null;
+    }
+    
+    @Override
+    public void signalHandshakeSuccess(Context context)
+    {
+        String homePage = getHomePage();
+        if (homePage != null)
+        {
+            dismissSplashScreen();
+
+            ProxySettings.setLocalProxy(
+                    this,
+                    PsiphonData.getPsiphonData().getHttpProxyPort());
+
+            m_webView.loadUrl(homePage);
+        }
+    }
+
+    @Override
+    public void signalUnexpectedDisconnect(Context context)
+    {
+        showSplashScreen();
+    }
+
+    @Override
+    public void signalTunnelStarting(Context context)
+    {
+    }
+
+    @Override
+    public void signalTunnelStopping(Context context)
+    {
+    }
+
+    @Override
+    public Intent pendingSignalNotification(Context context)
+    {
+        return null;
+    }
+}