Make ToyVpn a little more realistic

Now it shows how to:
- start as an always-on VPN.
- take over the last connection using protect().

Bug: 35802839
Test: manual connection
Change-Id: I4699afbcf4bd0933dbeb3bf77a2d91f49d6ede1d
diff --git a/samples/ToyVpn/res/drawable/ic_vpn.xml b/samples/ToyVpn/res/drawable/ic_vpn.xml
new file mode 100644
index 0000000..bc41788
--- /dev/null
+++ b/samples/ToyVpn/res/drawable/ic_vpn.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2017 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:height="24dp"
+    android:width="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24">
+
+    <path
+        android:fillColor="#000000"
+        android:pathData="M19.35,10.04 C18.67,6.59,15.64,4,12,4 C9.11,4,6.6,5.64,5.35,8.04
+C2.34,8.36,0,10.91,0,14 C0,17.31,2.69,20,6,20 L19,20 C21.76,20,24,17.76,24,15
+C24,12.36,21.95,10.22,19.35,10.04 Z M19,18 L6,18 C3.79,18,2,16.21,2,14
+S3.79,10,6,10 L6.71,10 C7.37,7.69,9.48,6,12,6 C15.04,6,17.5,8.46,17.5,11.5
+L17.5,12 L19,12 C20.66,12,22,13.34,22,15 S20.66,18,19,18 Z" />
+    <path
+        android:strokeColor="#000000"
+        android:strokeWidth="2"
+        android:pathData="M6.58994,13.1803 C6.58994,13.1803,8.59173,15.8724,12.011,15.8726
+C15.2788,15.8728,17.3696,13.2502,17.3696,13.2502" />
+</vector>
diff --git a/samples/ToyVpn/res/layout/form.xml b/samples/ToyVpn/res/layout/form.xml
index 7a325db..0f62e17 100644
--- a/samples/ToyVpn/res/layout/form.xml
+++ b/samples/ToyVpn/res/layout/form.xml
@@ -26,12 +26,13 @@
         <EditText style="@style/item" android:id="@+id/address"/>
 
         <TextView style="@style/item" android:text="@string/port"/>
-        <EditText style="@style/item" android:id="@+id/port"/>
+        <EditText style="@style/item" android:id="@+id/port" android:inputType="number"/>
 
         <TextView style="@style/item" android:text="@string/secret"/>
         <EditText style="@style/item" android:id="@+id/secret" android:password="true"/>
 
         <Button style="@style/item" android:id="@+id/connect" android:text="@string/connect"/>
+        <Button style="@style/item" android:id="@+id/disconnect" android:text="@string/disconnect"/>
 
     </LinearLayout>
 </ScrollView>
diff --git a/samples/ToyVpn/res/values/strings.xml b/samples/ToyVpn/res/values/strings.xml
index 2fe40d2..d5e06ba 100644
--- a/samples/ToyVpn/res/values/strings.xml
+++ b/samples/ToyVpn/res/values/strings.xml
@@ -22,6 +22,7 @@
     <string name="port">Server Port:</string>
     <string name="secret">Shared Secret:</string>
     <string name="connect">Connect!</string>
+    <string name="disconnect">Disconnect!</string>
 
     <string name="connecting">ToyVPN is connecting...</string>
     <string name="connected">ToyVPN is connected!</string>
diff --git a/samples/ToyVpn/src/com/example/android/toyvpn/ToyVpnClient.java b/samples/ToyVpn/src/com/example/android/toyvpn/ToyVpnClient.java
index 925179a..c6a72e9 100644
--- a/samples/ToyVpn/src/com/example/android/toyvpn/ToyVpnClient.java
+++ b/samples/ToyVpn/src/com/example/android/toyvpn/ToyVpnClient.java
@@ -18,49 +18,60 @@
 
 import android.app.Activity;
 import android.content.Intent;
+import android.content.SharedPreferences;
 import android.net.VpnService;
 import android.os.Bundle;
-import android.util.Log;
-import android.view.View;
 import android.widget.TextView;
-import android.widget.Button;
 
-public class ToyVpnClient extends Activity implements View.OnClickListener {
-    private TextView mServerAddress;
-    private TextView mServerPort;
-    private TextView mSharedSecret;
+public class ToyVpnClient extends Activity {
+    public interface Prefs {
+        String NAME = "connection";
+        String SERVER_ADDRESS = "server.address";
+        String SERVER_PORT = "server.port";
+        String SHARED_SECRET = "shared.secret";
+    }
 
     @Override
     public void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
         setContentView(R.layout.form);
 
-        mServerAddress = (TextView) findViewById(R.id.address);
-        mServerPort = (TextView) findViewById(R.id.port);
-        mSharedSecret = (TextView) findViewById(R.id.secret);
+        final TextView serverAddress = (TextView) findViewById(R.id.address);
+        final TextView serverPort = (TextView) findViewById(R.id.port);
+        final TextView sharedSecret = (TextView) findViewById(R.id.secret);
 
-        findViewById(R.id.connect).setOnClickListener(this);
-    }
+        final SharedPreferences prefs = getSharedPreferences(Prefs.NAME, MODE_PRIVATE);
+        serverAddress.setText(prefs.getString(Prefs.SERVER_ADDRESS, ""));
+        serverPort.setText(prefs.getString(Prefs.SERVER_PORT, ""));
+        sharedSecret.setText(prefs.getString(Prefs.SHARED_SECRET, ""));
 
-    @Override
-    public void onClick(View v) {
-        Intent intent = VpnService.prepare(this);
-        if (intent != null) {
-            startActivityForResult(intent, 0);
-        } else {
-            onActivityResult(0, RESULT_OK, null);
-        }
+        findViewById(R.id.connect).setOnClickListener(v -> {
+            prefs.edit()
+                    .putString(Prefs.SERVER_ADDRESS, serverAddress.getText().toString())
+                    .putString(Prefs.SERVER_PORT, serverPort.getText().toString())
+                    .putString(Prefs.SHARED_SECRET, sharedSecret.getText().toString())
+                    .commit();
+
+            Intent intent = VpnService.prepare(ToyVpnClient.this);
+            if (intent != null) {
+                startActivityForResult(intent, 0);
+            } else {
+                onActivityResult(0, RESULT_OK, null);
+            }
+        });
+        findViewById(R.id.disconnect).setOnClickListener(v -> {
+            startService(getServiceIntent().setAction(ToyVpnService.ACTION_DISCONNECT));
+        });
     }
 
     @Override
     protected void onActivityResult(int request, int result, Intent data) {
         if (result == RESULT_OK) {
-            String prefix = getPackageName();
-            Intent intent = new Intent(this, ToyVpnService.class)
-                    .putExtra(prefix + ".ADDRESS", mServerAddress.getText().toString())
-                    .putExtra(prefix + ".PORT", mServerPort.getText().toString())
-                    .putExtra(prefix + ".SECRET", mSharedSecret.getText().toString());
-            startService(intent);
+            startService(getServiceIntent().setAction(ToyVpnService.ACTION_CONNECT));
         }
     }
+
+    private Intent getServiceIntent() {
+        return new Intent(this, ToyVpnService.class);
+    }
 }
diff --git a/samples/ToyVpn/src/com/example/android/toyvpn/ToyVpnConnection.java b/samples/ToyVpn/src/com/example/android/toyvpn/ToyVpnConnection.java
new file mode 100644
index 0000000..6644ecd
--- /dev/null
+++ b/samples/ToyVpn/src/com/example/android/toyvpn/ToyVpnConnection.java
@@ -0,0 +1,328 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.toyvpn;
+
+import static java.nio.charset.StandardCharsets.US_ASCII;
+
+import android.app.PendingIntent;
+import android.net.VpnService;
+import android.os.ParcelFileDescriptor;
+import android.util.Log;
+
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.net.SocketAddress;
+import java.net.SocketException;
+import java.nio.ByteBuffer;
+import java.nio.channels.DatagramChannel;
+import java.util.concurrent.TimeUnit;
+
+public class ToyVpnConnection implements Runnable {
+    /**
+     * Callback interface to let the {@link ToyVpnService} know about new connections
+     * and update the foreground notification with connection status.
+     */
+    public interface OnEstablishListener {
+        void onEstablish(ParcelFileDescriptor tunInterface);
+    }
+
+    /** Maximum packet size is constrained by the MTU, which is given as a signed short. */
+    private static final int MAX_PACKET_SIZE = Short.MAX_VALUE;
+
+    /** Time to wait in between losing the connection and retrying. */
+    private static final long RECONNECT_WAIT_MS = TimeUnit.SECONDS.toMillis(3);
+
+    /** Time between keepalives if there is no traffic at the moment.
+     *
+     * TODO: don't do this; it's much better to let the connection die and then reconnect when
+     *       necessary instead of keeping the network hardware up for hours on end in between.
+     **/
+    private static final long KEEPALIVE_INTERVAL_MS = TimeUnit.SECONDS.toMillis(15);
+
+    /** Time to wait without receiving any response before assuming the server is gone. */
+    private static final long RECEIVE_TIMEOUT_MS = TimeUnit.SECONDS.toMillis(20);
+
+    /**
+     * Time between polling the VPN interface for new traffic, since it's non-blocking.
+     *
+     * TODO: really don't do this; a blocking read on another thread is much cleaner.
+     */
+    private static final long IDLE_INTERVAL_MS = TimeUnit.MILLISECONDS.toMillis(100);
+
+    /**
+     * Number of periods of length {@IDLE_INTERVAL_MS} to wait before declaring the handshake a
+     * complete and abject failure.
+     *
+     * TODO: use a higher-level protocol; hand-rolling is a fun but pointless exercise.
+     */
+    private static final int MAX_HANDSHAKE_ATTEMPTS = 50;
+
+    private final VpnService mService;
+    private final int mConnectionId;
+
+    private final String mServerName;
+    private final int mServerPort;
+    private final byte[] mSharedSecret;
+
+    private PendingIntent mConfigureIntent;
+    private OnEstablishListener mOnEstablishListener;
+
+    public ToyVpnConnection(final VpnService service, final int connectionId,
+            final String serverName, final int serverPort, final byte[] sharedSecret) {
+        mService = service;
+        mConnectionId = connectionId;
+
+        mServerName = serverName;
+        mServerPort= serverPort;
+        mSharedSecret = sharedSecret;
+    }
+
+    /**
+     * Optionally, set an intent to configure the VPN. This is {@code null} by default.
+     */
+    public void setConfigureIntent(PendingIntent intent) {
+        mConfigureIntent = intent;
+    }
+
+    public void setOnEstablishListener(OnEstablishListener listener) {
+        mOnEstablishListener = listener;
+    }
+
+    @Override
+    public void run() {
+        try {
+            Log.i(getTag(), "Starting");
+
+            // If anything needs to be obtained using the network, get it now.
+            // This greatly reduces the complexity of seamless handover, which
+            // tries to recreate the tunnel without shutting down everything.
+            // In this demo, all we need to know is the server address.
+            final SocketAddress serverAddress = new InetSocketAddress(mServerName, mServerPort);
+
+            // We try to create the tunnel several times.
+            // TODO: The better way is to work with ConnectivityManager, trying only when the
+            //       network is available.
+            // Here we just use a counter to keep things simple.
+            for (int attempt = 0; attempt < 10; ++attempt) {
+                // Reset the counter if we were connected.
+                if (run(serverAddress)) {
+                    attempt = 0;
+                }
+
+                // Sleep for a while. This also checks if we got interrupted.
+                Thread.sleep(3000);
+            }
+            Log.i(getTag(), "Giving up");
+        } catch (IOException | InterruptedException | IllegalArgumentException e) {
+            Log.e(getTag(), "Connection failed, exiting", e);
+        }
+    }
+
+    private boolean run(SocketAddress server)
+            throws IOException, InterruptedException, IllegalArgumentException {
+        ParcelFileDescriptor iface = null;
+        boolean connected = false;
+        // Create a DatagramChannel as the VPN tunnel.
+        try (DatagramChannel tunnel = DatagramChannel.open()) {
+
+            // Protect the tunnel before connecting to avoid loopback.
+            if (!mService.protect(tunnel.socket())) {
+                throw new IllegalStateException("Cannot protect the tunnel");
+            }
+
+            // Connect to the server.
+            tunnel.connect(server);
+
+            // For simplicity, we use the same thread for both reading and
+            // writing. Here we put the tunnel into non-blocking mode.
+            tunnel.configureBlocking(false);
+
+            // Authenticate and configure the virtual network interface.
+            iface = handshake(tunnel);
+
+            // Now we are connected. Set the flag.
+            connected = true;
+
+            // Packets to be sent are queued in this input stream.
+            FileInputStream in = new FileInputStream(iface.getFileDescriptor());
+
+            // Packets received need to be written to this output stream.
+            FileOutputStream out = new FileOutputStream(iface.getFileDescriptor());
+
+            // Allocate the buffer for a single packet.
+            ByteBuffer packet = ByteBuffer.allocate(MAX_PACKET_SIZE);
+
+            // Timeouts:
+            //   - when data has not been sent in a while, send empty keepalive messages.
+            //   - when data has not been received in a while, assume the connection is broken.
+            long lastSendTime = System.currentTimeMillis();
+            long lastReceiveTime = System.currentTimeMillis();
+
+            // We keep forwarding packets till something goes wrong.
+            while (true) {
+                // Assume that we did not make any progress in this iteration.
+                boolean idle = true;
+
+                // Read the outgoing packet from the input stream.
+                int length = in.read(packet.array());
+                if (length > 0) {
+                    // Write the outgoing packet to the tunnel.
+                    packet.limit(length);
+                    tunnel.write(packet);
+                    packet.clear();
+
+                    // There might be more outgoing packets.
+                    idle = false;
+                    lastReceiveTime = System.currentTimeMillis();
+                }
+
+                // Read the incoming packet from the tunnel.
+                length = tunnel.read(packet);
+                if (length > 0) {
+                    // Ignore control messages, which start with zero.
+                    if (packet.get(0) != 0) {
+                        // Write the incoming packet to the output stream.
+                        out.write(packet.array(), 0, length);
+                    }
+                    packet.clear();
+
+                    // There might be more incoming packets.
+                    idle = false;
+                    lastSendTime = System.currentTimeMillis();
+                }
+
+                // If we are idle or waiting for the network, sleep for a
+                // fraction of time to avoid busy looping.
+                if (idle) {
+                    Thread.sleep(IDLE_INTERVAL_MS);
+                    final long timeNow = System.currentTimeMillis();
+
+                    if (lastSendTime + KEEPALIVE_INTERVAL_MS <= timeNow) {
+                        // We are receiving for a long time but not sending.
+                        // Send empty control messages.
+                        packet.put((byte) 0).limit(1);
+                        for (int i = 0; i < 3; ++i) {
+                            packet.position(0);
+                            tunnel.write(packet);
+                        }
+                        packet.clear();
+                        lastSendTime = timeNow;
+                    } else if (lastReceiveTime + RECEIVE_TIMEOUT_MS <= timeNow) {
+                        // We are sending for a long time but not receiving.
+                        throw new IllegalStateException("Timed out");
+                    }
+                }
+            }
+        } catch (SocketException e) {
+            Log.e(getTag(), "Cannot use socket", e);
+        } finally {
+            if (iface != null) {
+                try {
+                    iface.close();
+                } catch (IOException e) {
+                    Log.e(getTag(), "Unable to close interface", e);
+                }
+            }
+        }
+        return connected;
+    }
+
+    private ParcelFileDescriptor handshake(DatagramChannel tunnel)
+            throws IOException, InterruptedException {
+        // To build a secured tunnel, we should perform mutual authentication
+        // and exchange session keys for encryption. To keep things simple in
+        // this demo, we just send the shared secret in plaintext and wait
+        // for the server to send the parameters.
+
+        // Allocate the buffer for handshaking. We have a hardcoded maximum
+        // handshake size of 1024 bytes, which should be enough for demo
+        // purposes.
+        ByteBuffer packet = ByteBuffer.allocate(1024);
+
+        // Control messages always start with zero.
+        packet.put((byte) 0).put(mSharedSecret).flip();
+
+        // Send the secret several times in case of packet loss.
+        for (int i = 0; i < 3; ++i) {
+            packet.position(0);
+            tunnel.write(packet);
+        }
+        packet.clear();
+
+        // Wait for the parameters within a limited time.
+        for (int i = 0; i < MAX_HANDSHAKE_ATTEMPTS; ++i) {
+            Thread.sleep(IDLE_INTERVAL_MS);
+
+            // Normally we should not receive random packets. Check that the first
+            // byte is 0 as expected.
+            int length = tunnel.read(packet);
+            if (length > 0 && packet.get(0) == 0) {
+                return configure(new String(packet.array(), 1, length - 1, US_ASCII).trim());
+            }
+        }
+        throw new IOException("Timed out");
+    }
+
+    private ParcelFileDescriptor configure(String parameters) throws IllegalArgumentException {
+        // Configure a builder while parsing the parameters.
+        VpnService.Builder builder = mService.new Builder();
+        for (String parameter : parameters.split(" ")) {
+            String[] fields = parameter.split(",");
+            try {
+                switch (fields[0].charAt(0)) {
+                    case 'm':
+                        builder.setMtu(Short.parseShort(fields[1]));
+                        break;
+                    case 'a':
+                        builder.addAddress(fields[1], Integer.parseInt(fields[2]));
+                        break;
+                    case 'r':
+                        builder.addRoute(fields[1], Integer.parseInt(fields[2]));
+                        break;
+                    case 'd':
+                        builder.addDnsServer(fields[1]);
+                        break;
+                    case 's':
+                        builder.addSearchDomain(fields[1]);
+                        break;
+                }
+            } catch (NumberFormatException e) {
+                throw new IllegalArgumentException("Bad parameter: " + parameter);
+            }
+        }
+
+        // Create a new interface using the builder and save the parameters.
+        final ParcelFileDescriptor vpnInterface;
+        synchronized (mService) {
+            vpnInterface = builder
+                    .setSession(mServerName)
+                    .setConfigureIntent(mConfigureIntent)
+                    .establish();
+            if (mOnEstablishListener != null) {
+                mOnEstablishListener.onEstablish(vpnInterface);
+            }
+        }
+        Log.i(getTag(), "New interface: " + vpnInterface + " (" + parameters + ")");
+        return vpnInterface;
+    }
+
+    private final String getTag() {
+        return ToyVpnConnection.class.getSimpleName() + "[" + mConnectionId + "]";
+    }
+}
diff --git a/samples/ToyVpn/src/com/example/android/toyvpn/ToyVpnService.java b/samples/ToyVpn/src/com/example/android/toyvpn/ToyVpnService.java
index 41cf0e1..5e42d9d 100644
--- a/samples/ToyVpn/src/com/example/android/toyvpn/ToyVpnService.java
+++ b/samples/ToyVpn/src/com/example/android/toyvpn/ToyVpnService.java
@@ -16,322 +16,153 @@
 
 package com.example.android.toyvpn;
 
+import android.app.Notification;
 import android.app.PendingIntent;
 import android.app.Service;
 import android.content.Intent;
+import android.content.SharedPreferences;
 import android.net.VpnService;
 import android.os.Handler;
 import android.os.Message;
 import android.os.ParcelFileDescriptor;
 import android.util.Log;
+import android.util.Pair;
 import android.widget.Toast;
 
-import java.io.FileInputStream;
-import java.io.FileOutputStream;
-import java.net.InetSocketAddress;
-import java.nio.ByteBuffer;
-import java.nio.channels.DatagramChannel;
+import java.io.IOException;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
 
-public class ToyVpnService extends VpnService implements Handler.Callback, Runnable {
-    private static final String TAG = "ToyVpnService";
+public class ToyVpnService extends VpnService implements Handler.Callback {
+    private static final String TAG = ToyVpnService.class.getSimpleName();
 
-    private String mServerAddress;
-    private String mServerPort;
-    private byte[] mSharedSecret;
-    private PendingIntent mConfigureIntent;
+    public static final String ACTION_CONNECT = "com.example.android.toyvpn.START";
+    public static final String ACTION_DISCONNECT = "com.example.android.toyvpn.STOP";
 
     private Handler mHandler;
-    private Thread mThread;
 
-    private ParcelFileDescriptor mInterface;
-    private String mParameters;
+    private static class Connection extends Pair<Thread, ParcelFileDescriptor> {
+        public Connection(Thread thread, ParcelFileDescriptor pfd) {
+            super(thread, pfd);
+        }
+    }
+
+    private final AtomicReference<Thread> mConnectingThread = new AtomicReference<>();
+    private final AtomicReference<Connection> mConnection = new AtomicReference<>();
+
+    private AtomicInteger mNextConnectionId = new AtomicInteger(1);
+
+    private PendingIntent mConfigureIntent;
 
     @Override
-    public int onStartCommand(Intent intent, int flags, int startId) {
+    public void onCreate() {
         // The handler is only used to show messages.
         if (mHandler == null) {
             mHandler = new Handler(this);
         }
 
-        // Stop the previous session by interrupting the thread.
-        if (mThread != null) {
-            mThread.interrupt();
+        // Create the intent to "configure" the connection (just start ToyVpnClient).
+        mConfigureIntent = PendingIntent.getActivity(this, 0, new Intent(this, ToyVpnClient.class),
+                PendingIntent.FLAG_UPDATE_CURRENT);
+    }
+
+    @Override
+    public int onStartCommand(Intent intent, int flags, int startId) {
+        if (intent != null && ACTION_DISCONNECT.equals(intent.getAction())) {
+            disconnect();
+            return START_NOT_STICKY;
+        } else {
+            connect();
+            return START_STICKY;
         }
-
-        // Extract information from the intent.
-        String prefix = getPackageName();
-        mServerAddress = intent.getStringExtra(prefix + ".ADDRESS");
-        mServerPort = intent.getStringExtra(prefix + ".PORT");
-        mSharedSecret = intent.getStringExtra(prefix + ".SECRET").getBytes();
-
-        // Start a new session by creating a new thread.
-        mThread = new Thread(this, "ToyVpnThread");
-        mThread.start();
-        return START_STICKY;
     }
 
     @Override
     public void onDestroy() {
-        if (mThread != null) {
-            mThread.interrupt();
-        }
+        disconnect();
     }
 
     @Override
     public boolean handleMessage(Message message) {
-        if (message != null) {
-            Toast.makeText(this, message.what, Toast.LENGTH_SHORT).show();
+        Toast.makeText(this, message.what, Toast.LENGTH_SHORT).show();
+        if (message.what != R.string.disconnected) {
+            updateForegroundNotification(message.what);
         }
         return true;
     }
 
-    @Override
-    public synchronized void run() {
+    private void connect() {
+        // Become a foreground service. Background services can be VPN services too, but they can
+        // be killed by background check before getting a chance to receive onRevoke().
+        updateForegroundNotification(R.string.connecting);
+        mHandler.sendEmptyMessage(R.string.connecting);
+
+        // Extract information from the shared preferences.
+        final SharedPreferences prefs = getSharedPreferences(ToyVpnClient.Prefs.NAME, MODE_PRIVATE);
+        final String server = prefs.getString(ToyVpnClient.Prefs.SERVER_ADDRESS, "");
+        final byte[] secret = prefs.getString(ToyVpnClient.Prefs.SHARED_SECRET, "").getBytes();
+        final int port;
         try {
-            Log.i(TAG, "Starting");
-
-            // If anything needs to be obtained using the network, get it now.
-            // This greatly reduces the complexity of seamless handover, which
-            // tries to recreate the tunnel without shutting down everything.
-            // In this demo, all we need to know is the server address.
-            InetSocketAddress server = new InetSocketAddress(
-                    mServerAddress, Integer.parseInt(mServerPort));
-
-            // We try to create the tunnel for several times. The better way
-            // is to work with ConnectivityManager, such as trying only when
-            // the network is avaiable. Here we just use a counter to keep
-            // things simple.
-            for (int attempt = 0; attempt < 10; ++attempt) {
-                mHandler.sendEmptyMessage(R.string.connecting);
-
-                // Reset the counter if we were connected.
-                if (run(server)) {
-                    attempt = 0;
-                }
-
-                // Sleep for a while. This also checks if we got interrupted.
-                Thread.sleep(3000);
-            }
-            Log.i(TAG, "Giving up");
-        } catch (Exception e) {
-            Log.e(TAG, "Got " + e.toString());
-        } finally {
-            try {
-                mInterface.close();
-            } catch (Exception e) {
-                // ignore
-            }
-            mInterface = null;
-            mParameters = null;
-
-            mHandler.sendEmptyMessage(R.string.disconnected);
-            Log.i(TAG, "Exiting");
-        }
-    }
-
-    private boolean run(InetSocketAddress server) throws Exception {
-        DatagramChannel tunnel = null;
-        boolean connected = false;
-        try {
-            // Create a DatagramChannel as the VPN tunnel.
-            tunnel = DatagramChannel.open();
-
-            // Protect the tunnel before connecting to avoid loopback.
-            if (!protect(tunnel.socket())) {
-                throw new IllegalStateException("Cannot protect the tunnel");
-            }
-
-            // Connect to the server.
-            tunnel.connect(server);
-
-            // For simplicity, we use the same thread for both reading and
-            // writing. Here we put the tunnel into non-blocking mode.
-            tunnel.configureBlocking(false);
-
-            // Authenticate and configure the virtual network interface.
-            handshake(tunnel);
-
-            // Now we are connected. Set the flag and show the message.
-            connected = true;
-            mHandler.sendEmptyMessage(R.string.connected);
-
-            // Packets to be sent are queued in this input stream.
-            FileInputStream in = new FileInputStream(mInterface.getFileDescriptor());
-
-            // Packets received need to be written to this output stream.
-            FileOutputStream out = new FileOutputStream(mInterface.getFileDescriptor());
-
-            // Allocate the buffer for a single packet.
-            ByteBuffer packet = ByteBuffer.allocate(32767);
-
-            // We use a timer to determine the status of the tunnel. It
-            // works on both sides. A positive value means sending, and
-            // any other means receiving. We start with receiving.
-            int timer = 0;
-
-            // We keep forwarding packets till something goes wrong.
-            while (true) {
-                // Assume that we did not make any progress in this iteration.
-                boolean idle = true;
-
-                // Read the outgoing packet from the input stream.
-                int length = in.read(packet.array());
-                if (length > 0) {
-                    // Write the outgoing packet to the tunnel.
-                    packet.limit(length);
-                    tunnel.write(packet);
-                    packet.clear();
-
-                    // There might be more outgoing packets.
-                    idle = false;
-
-                    // If we were receiving, switch to sending.
-                    if (timer < 1) {
-                        timer = 1;
-                    }
-                }
-
-                // Read the incoming packet from the tunnel.
-                length = tunnel.read(packet);
-                if (length > 0) {
-                    // Ignore control messages, which start with zero.
-                    if (packet.get(0) != 0) {
-                        // Write the incoming packet to the output stream.
-                        out.write(packet.array(), 0, length);
-                    }
-                    packet.clear();
-
-                    // There might be more incoming packets.
-                    idle = false;
-
-                    // If we were sending, switch to receiving.
-                    if (timer > 0) {
-                        timer = 0;
-                    }
-                }
-
-                // If we are idle or waiting for the network, sleep for a
-                // fraction of time to avoid busy looping.
-                if (idle) {
-                    Thread.sleep(100);
-
-                    // Increase the timer. This is inaccurate but good enough,
-                    // since everything is operated in non-blocking mode.
-                    timer += (timer > 0) ? 100 : -100;
-
-                    // We are receiving for a long time but not sending.
-                    if (timer < -15000) {
-                        // Send empty control messages.
-                        packet.put((byte) 0).limit(1);
-                        for (int i = 0; i < 3; ++i) {
-                            packet.position(0);
-                            tunnel.write(packet);
-                        }
-                        packet.clear();
-
-                        // Switch to sending.
-                        timer = 1;
-                    }
-
-                    // We are sending for a long time but not receiving.
-                    if (timer > 20000) {
-                        throw new IllegalStateException("Timed out");
-                    }
-                }
-            }
-        } catch (InterruptedException e) {
-            throw e;
-        } catch (Exception e) {
-            Log.e(TAG, "Got " + e.toString());
-        } finally {
-            try {
-                tunnel.close();
-            } catch (Exception e) {
-                // ignore
-            }
-        }
-        return connected;
-    }
-
-    private void handshake(DatagramChannel tunnel) throws Exception {
-        // To build a secured tunnel, we should perform mutual authentication
-        // and exchange session keys for encryption. To keep things simple in
-        // this demo, we just send the shared secret in plaintext and wait
-        // for the server to send the parameters.
-
-        // Allocate the buffer for handshaking.
-        ByteBuffer packet = ByteBuffer.allocate(1024);
-
-        // Control messages always start with zero.
-        packet.put((byte) 0).put(mSharedSecret).flip();
-
-        // Send the secret several times in case of packet loss.
-        for (int i = 0; i < 3; ++i) {
-            packet.position(0);
-            tunnel.write(packet);
-        }
-        packet.clear();
-
-        // Wait for the parameters within a limited time.
-        for (int i = 0; i < 50; ++i) {
-            Thread.sleep(100);
-
-            // Normally we should not receive random packets.
-            int length = tunnel.read(packet);
-            if (length > 0 && packet.get(0) == 0) {
-                configure(new String(packet.array(), 1, length - 1).trim());
-                return;
-            }
-        }
-        throw new IllegalStateException("Timed out");
-    }
-
-    private void configure(String parameters) throws Exception {
-        // If the old interface has exactly the same parameters, use it!
-        if (mInterface != null && parameters.equals(mParameters)) {
-            Log.i(TAG, "Using the previous interface");
+            port = Integer.parseInt(prefs.getString(ToyVpnClient.Prefs.SERVER_PORT, ""));
+        } catch (NumberFormatException e) {
+            Log.e(TAG, "Bad port: " + prefs.getString(ToyVpnClient.Prefs.SERVER_PORT, null), e);
             return;
         }
 
-        // Configure a builder while parsing the parameters.
-        Builder builder = new Builder();
-        for (String parameter : parameters.split(" ")) {
-            String[] fields = parameter.split(",");
+        // Kick off a connection.
+        startConnection(new ToyVpnConnection(
+                this, mNextConnectionId.getAndIncrement(), server, port, secret));
+    }
+
+    private void startConnection(final ToyVpnConnection connection) {
+        // Replace any existing connecting thread with the  new one.
+        final Thread thread = new Thread(connection, "ToyVpnThread");
+        setConnectingThread(thread);
+
+        // Handler to mark as connected once onEstablish is called.
+        connection.setConfigureIntent(mConfigureIntent);
+        connection.setOnEstablishListener(new ToyVpnConnection.OnEstablishListener() {
+            public void onEstablish(ParcelFileDescriptor tunInterface) {
+                mHandler.sendEmptyMessage(R.string.connected);
+
+                mConnectingThread.compareAndSet(thread, null);
+                setConnection(new Connection(thread, tunInterface));
+            }
+        });
+        thread.start();
+    }
+
+    private void setConnectingThread(final Thread thread) {
+        final Thread oldThread = mConnectingThread.getAndSet(thread);
+        if (oldThread != null) {
+            oldThread.interrupt();
+        }
+    }
+
+    private void setConnection(final Connection connection) {
+        final Connection oldConnection = mConnection.getAndSet(connection);
+        if (oldConnection != null) {
             try {
-                switch (fields[0].charAt(0)) {
-                    case 'm':
-                        builder.setMtu(Short.parseShort(fields[1]));
-                        break;
-                    case 'a':
-                        builder.addAddress(fields[1], Integer.parseInt(fields[2]));
-                        break;
-                    case 'r':
-                        builder.addRoute(fields[1], Integer.parseInt(fields[2]));
-                        break;
-                    case 'd':
-                        builder.addDnsServer(fields[1]);
-                        break;
-                    case 's':
-                        builder.addSearchDomain(fields[1]);
-                        break;
-                }
-            } catch (Exception e) {
-                throw new IllegalArgumentException("Bad parameter: " + parameter);
+                oldConnection.first.interrupt();
+                oldConnection.second.close();
+            } catch (IOException e) {
+                Log.e(TAG, "Closing VPN interface", e);
             }
         }
+    }
 
-        // Close the old interface since the parameters have been changed.
-        try {
-            mInterface.close();
-        } catch (Exception e) {
-            // ignore
-        }
+    private void disconnect() {
+        mHandler.sendEmptyMessage(R.string.disconnected);
+        setConnectingThread(null);
+        setConnection(null);
+        stopForeground(true);
+    }
 
-        // Create a new interface using the builder and save the parameters.
-        mInterface = builder.setSession(mServerAddress)
-                .setConfigureIntent(mConfigureIntent)
-                .establish();
-        mParameters = parameters;
-        Log.i(TAG, "New interface: " + parameters);
+    private void updateForegroundNotification(final int message) {
+        startForeground(1, new Notification.Builder(this)
+                .setSmallIcon(R.drawable.ic_vpn)
+                .setContentText(getString(message))
+                .setContentIntent(mConfigureIntent)
+                .build());
     }
 }