Simplify the VPN service implementation.

+ Remove NormalProcessProxy and ProcessProxy as they are not used
  anymore.
+ Rename AndroidServiceProxy to DaemonProxy and simplify its
  implementation as it does not extend to ProcessProxy anymore.
+ Execute connect() in VpnService in one thread, which simplifies socket
  and error handling.
+ Modify service subclasses accordingly.
+ Execute connect() and disconnect() in VpnServiceBinder so that the
  operations do not block the UI thread. Mark service as foreground only upon
  connecting.
diff --git a/packages/VpnServices/src/com/android/server/vpn/AndroidServiceProxy.java b/packages/VpnServices/src/com/android/server/vpn/AndroidServiceProxy.java
deleted file mode 100644
index e4c070f..0000000
--- a/packages/VpnServices/src/com/android/server/vpn/AndroidServiceProxy.java
+++ /dev/null
@@ -1,261 +0,0 @@
-/*
- * Copyright (C) 2009, 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.android.server.vpn;
-
-import android.net.LocalSocket;
-import android.net.LocalSocketAddress;
-import android.net.vpn.VpnManager;
-import android.os.SystemProperties;
-import android.util.Log;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-
-/**
- * Proxy to start, stop and interact with an Android service defined in init.rc.
- * The android service is expected to accept connection through Unix domain
- * socket. When the proxy successfully starts the service, it will establish a
- * socket connection with the service. The socket serves two purposes: (1) send
- * commands to the service; (2) for the proxy to know whether the service is
- * alive.
- *
- * After the service receives commands from the proxy, it should return either
- * 0 if the service will close the socket (and the proxy will re-establish
- * another connection immediately after), or 1 if the socket is remained alive.
- */
-public class AndroidServiceProxy extends ProcessProxy {
-    private static final int WAITING_TIME = 15; // sec
-
-    private static final String SVC_STATE_CMD_PREFIX = "init.svc.";
-    private static final String SVC_START_CMD = "ctl.start";
-    private static final String SVC_STOP_CMD = "ctl.stop";
-    private static final String SVC_STATE_RUNNING = "running";
-    private static final String SVC_STATE_STOPPED = "stopped";
-
-    private static final int END_OF_ARGUMENTS = 255;
-
-    private static final int STOP_SERVICE = -1;
-    private static final int AUTH_ERROR_CODE = 51;
-
-    private String mServiceName;
-    private String mSocketName;
-    private LocalSocket mKeepaliveSocket;
-    private boolean mControlSocketInUse;
-    private Integer mSocketResult = null;
-    private String mTag;
-
-    /**
-     * Creates a proxy with the service name.
-     * @param serviceName the service name
-     */
-    public AndroidServiceProxy(String serviceName) {
-        mServiceName = serviceName;
-        mSocketName = serviceName;
-        mTag = "SProxy_" + serviceName;
-    }
-
-    @Override
-    public String getName() {
-        return "Service " + mServiceName;
-    }
-
-    @Override
-    public synchronized void stop() {
-        if (isRunning()) {
-            try {
-                setResultAndCloseControlSocket(STOP_SERVICE);
-            } catch (IOException e) {
-                // should not occur
-                throw new RuntimeException(e);
-            }
-        }
-        Log.d(mTag, "-----  Stop: " + mServiceName);
-        SystemProperties.set(SVC_STOP_CMD, mServiceName);
-    }
-
-    /**
-     * Sends a command with arguments to the service through the control socket.
-     */
-    public synchronized void sendCommand(String ...args) throws IOException {
-        OutputStream out = getControlSocketOutput();
-        for (String arg : args) outputString(out, arg);
-        out.write(END_OF_ARGUMENTS);
-        out.flush();
-        checkSocketResult();
-    }
-
-    /**
-     * {@inheritDoc}
-     * The method returns when the service exits.
-     */
-    @Override
-    protected void performTask() throws IOException {
-        String svc = mServiceName;
-        Log.d(mTag, "-----  Stop the daemon just in case: " + mServiceName);
-        SystemProperties.set(SVC_STOP_CMD, mServiceName);
-        if (!blockUntil(SVC_STATE_STOPPED, 5)) {
-            throw new IOException("cannot start service anew: " + svc
-                    + ", it is still running");
-        }
-
-        Log.d(mTag, "+++++  Start: " + svc);
-        SystemProperties.set(SVC_START_CMD, svc);
-
-        boolean success = blockUntil(SVC_STATE_RUNNING, WAITING_TIME);
-
-        if (success) {
-            Log.d(mTag, "-----  Running: " + svc + ", create keepalive socket");
-            LocalSocket s = mKeepaliveSocket = createServiceSocket();
-            setState(ProcessState.RUNNING);
-
-            if (s == null) {
-                // no socket connection, stop hosting the service
-                stop();
-                return;
-            }
-            try {
-                for (;;) {
-                    InputStream in = s.getInputStream();
-                    int data = in.read();
-                    if (data >= 0) {
-                        Log.d(mTag, "got data from control socket: " + data);
-
-                        setSocketResult(data);
-                    } else {
-                        // service is gone
-                        if (mControlSocketInUse) setSocketResult(-1);
-                        break;
-                    }
-                }
-                Log.d(mTag, "control connection closed");
-            } catch (IOException e) {
-                if (e instanceof VpnConnectingError) {
-                    throw e;
-                } else {
-                    Log.d(mTag, "control socket broken: " + e.getMessage());
-                }
-            }
-
-            // Wait 5 seconds for the service to exit
-            success = blockUntil(SVC_STATE_STOPPED, 5);
-            Log.d(mTag, "stopping " + svc + ", success? " + success);
-        } else {
-            setState(ProcessState.STOPPED);
-            throw new IOException("cannot start service: " + svc);
-        }
-    }
-
-    private LocalSocket createServiceSocket() throws IOException {
-        LocalSocket s = new LocalSocket();
-        LocalSocketAddress a = new LocalSocketAddress(mSocketName,
-                LocalSocketAddress.Namespace.RESERVED);
-
-        // try a few times in case the service has not listen()ed
-        IOException excp = null;
-        for (int i = 0; i < 10; i++) {
-            try {
-                s.connect(a);
-                return s;
-            } catch (IOException e) {
-                Log.d(mTag, "service not yet listen()ing; try again");
-                excp = e;
-                sleep(500);
-            }
-        }
-        throw excp;
-    }
-
-    private OutputStream getControlSocketOutput() throws IOException {
-        if (mKeepaliveSocket != null) {
-            mControlSocketInUse = true;
-            mSocketResult = null;
-            return mKeepaliveSocket.getOutputStream();
-        } else {
-            throw new IOException("no control socket available");
-        }
-    }
-
-    private void checkSocketResult() throws IOException {
-        try {
-            // will be notified when the result comes back from service
-            if (mSocketResult == null) wait();
-        } catch (InterruptedException e) {
-            Log.d(mTag, "checkSocketResult(): " + e);
-        } finally {
-            mControlSocketInUse = false;
-            if ((mSocketResult == null) || (mSocketResult < 0)) {
-                throw new IOException("socket error, result from service: "
-                        + mSocketResult);
-            }
-        }
-    }
-
-    private synchronized void setSocketResult(int result)
-            throws VpnConnectingError {
-        if (mControlSocketInUse) {
-            mSocketResult = result;
-            notifyAll();
-        } else if (result > 0) {
-            // error from daemon
-            throw new VpnConnectingError((result == AUTH_ERROR_CODE)
-                    ? VpnManager.VPN_ERROR_AUTH
-                    : VpnManager.VPN_ERROR_CONNECTION_FAILED);
-        }
-    }
-
-    private void setResultAndCloseControlSocket(int result)
-            throws VpnConnectingError {
-        setSocketResult(result);
-        try {
-            mKeepaliveSocket.shutdownInput();
-            mKeepaliveSocket.shutdownOutput();
-            mKeepaliveSocket.close();
-        } catch (IOException e) {
-            Log.e(mTag, "close keepalive socket", e);
-        } finally {
-            mKeepaliveSocket = null;
-        }
-    }
-
-    /**
-     * Waits for the process to be in the expected state. The method returns
-     * false if after the specified duration (in seconds), the process is still
-     * not in the expected state.
-     */
-    private boolean blockUntil(String expectedState, int waitTime) {
-        String cmd = SVC_STATE_CMD_PREFIX + mServiceName;
-        int sleepTime = 200; // ms
-        int n = waitTime * 1000 / sleepTime;
-        for (int i = 0; i < n; i++) {
-            if (expectedState.equals(SystemProperties.get(cmd))) {
-                Log.d(mTag, mServiceName + " is " + expectedState + " after "
-                        + (i * sleepTime) + " msec");
-                break;
-            }
-            sleep(sleepTime);
-        }
-        return expectedState.equals(SystemProperties.get(cmd));
-    }
-
-    private void outputString(OutputStream out, String s) throws IOException {
-        byte[] bytes = s.getBytes();
-        out.write(bytes.length);
-        out.write(bytes);
-        out.flush();
-    }
-}
diff --git a/packages/VpnServices/src/com/android/server/vpn/DaemonProxy.java b/packages/VpnServices/src/com/android/server/vpn/DaemonProxy.java
new file mode 100644
index 0000000..b749821
--- /dev/null
+++ b/packages/VpnServices/src/com/android/server/vpn/DaemonProxy.java
@@ -0,0 +1,199 @@
+/*
+ * Copyright (C) 2009, 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.android.server.vpn;
+
+import android.net.LocalSocket;
+import android.net.LocalSocketAddress;
+import android.net.vpn.VpnManager;
+import android.os.SystemProperties;
+import android.util.Log;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+/**
+ * Proxy to start, stop and interact with a VPN daemon.
+ * The daemon is expected to accept connection through Unix domain socket.
+ * When the proxy successfully starts the daemon, it will establish a socket
+ * connection with the daemon, to both send commands to the daemon and receive
+ * response and connecting error code from the daemon.
+ */
+class DaemonProxy {
+    private static final int WAITING_TIME = 15; // sec
+
+    private static final String SVC_STATE_CMD_PREFIX = "init.svc.";
+    private static final String SVC_START_CMD = "ctl.start";
+    private static final String SVC_STOP_CMD = "ctl.stop";
+    private static final String SVC_STATE_RUNNING = "running";
+    private static final String SVC_STATE_STOPPED = "stopped";
+
+    private static final int END_OF_ARGUMENTS = 255;
+
+    private String mName;
+    private LocalSocket mControlSocket;
+    private String mTag;
+
+    /**
+     * Creates a proxy of the specified daemon.
+     * @param daemonName name of the daemon
+     */
+    DaemonProxy(String daemonName) {
+        mName = daemonName;
+        mTag = "SProxy_" + daemonName;
+    }
+
+    String getName() {
+        return mName;
+    }
+
+    void start() throws IOException {
+        String svc = mName;
+        Log.d(mTag, "-----  Stop the daemon just in case: " + mName);
+        SystemProperties.set(SVC_STOP_CMD, mName);
+        if (!blockUntil(SVC_STATE_STOPPED, 5)) {
+            throw new IOException("cannot start service anew: " + svc
+                    + ", it is still running");
+        }
+
+        Log.d(mTag, "+++++  Start: " + svc);
+        SystemProperties.set(SVC_START_CMD, svc);
+
+        if (!blockUntil(SVC_STATE_RUNNING, WAITING_TIME)) {
+            throw new IOException("cannot start service: " + svc);
+        } else {
+            mControlSocket = createServiceSocket();
+        }
+    }
+
+    void sendCommand(String ...args) throws IOException {
+        OutputStream out = getControlSocketOutput();
+        for (String arg : args) outputString(out, arg);
+        out.write(END_OF_ARGUMENTS);
+        out.flush();
+
+        int result = getResultFromSocket(true);
+        if (result != args.length) {
+            throw new IOException("socket error, result from service: "
+                    + result);
+        }
+    }
+
+    // returns 0 if nothing is in the receive buffer
+    int getResultFromSocket() throws IOException {
+        return getResultFromSocket(false);
+    }
+
+    void closeControlSocket() {
+        if (mControlSocket == null) return;
+        try {
+            mControlSocket.close();
+        } catch (IOException e) {
+            Log.e(mTag, "close control socket", e);
+        } finally {
+            mControlSocket = null;
+        }
+    }
+
+    void stop() {
+        String svc = mName;
+        Log.d(mTag, "-----  Stop: " + svc);
+        SystemProperties.set(SVC_STOP_CMD, svc);
+        boolean success = blockUntil(SVC_STATE_STOPPED, 5);
+        Log.d(mTag, "stopping " + svc + ", success? " + success);
+    }
+
+    boolean isStopped() {
+        String cmd = SVC_STATE_CMD_PREFIX + mName;
+        return SVC_STATE_STOPPED.equals(SystemProperties.get(cmd));
+    }
+
+    private int getResultFromSocket(boolean blocking) throws IOException {
+        LocalSocket s = mControlSocket;
+        if (s == null) return 0;
+        InputStream in = s.getInputStream();
+        if (!blocking && in.available() == 0) return 0;
+
+        int data = in.read();
+        Log.d(mTag, "got data from control socket: " + data);
+
+        return data;
+    }
+
+    private LocalSocket createServiceSocket() throws IOException {
+        LocalSocket s = new LocalSocket();
+        LocalSocketAddress a = new LocalSocketAddress(mName,
+                LocalSocketAddress.Namespace.RESERVED);
+
+        // try a few times in case the service has not listen()ed
+        IOException excp = null;
+        for (int i = 0; i < 10; i++) {
+            try {
+                s.connect(a);
+                return s;
+            } catch (IOException e) {
+                Log.d(mTag, "service not yet listen()ing; try again");
+                excp = e;
+                sleep(500);
+            }
+        }
+        throw excp;
+    }
+
+    private OutputStream getControlSocketOutput() throws IOException {
+        if (mControlSocket != null) {
+            return mControlSocket.getOutputStream();
+        } else {
+            throw new IOException("no control socket available");
+        }
+    }
+
+    /**
+     * Waits for the process to be in the expected state. The method returns
+     * false if after the specified duration (in seconds), the process is still
+     * not in the expected state.
+     */
+    private boolean blockUntil(String expectedState, int waitTime) {
+        String cmd = SVC_STATE_CMD_PREFIX + mName;
+        int sleepTime = 200; // ms
+        int n = waitTime * 1000 / sleepTime;
+        for (int i = 0; i < n; i++) {
+            if (expectedState.equals(SystemProperties.get(cmd))) {
+                Log.d(mTag, mName + " is " + expectedState + " after "
+                        + (i * sleepTime) + " msec");
+                break;
+            }
+            sleep(sleepTime);
+        }
+        return expectedState.equals(SystemProperties.get(cmd));
+    }
+
+    private void outputString(OutputStream out, String s) throws IOException {
+        byte[] bytes = s.getBytes();
+        out.write(bytes.length);
+        out.write(bytes);
+        out.flush();
+    }
+
+    private void sleep(int msec) {
+        try {
+            Thread.currentThread().sleep(msec);
+        } catch (InterruptedException e) {
+            throw new RuntimeException(e);
+        }
+    }
+}
diff --git a/packages/VpnServices/src/com/android/server/vpn/L2tpIpsecPskService.java b/packages/VpnServices/src/com/android/server/vpn/L2tpIpsecPskService.java
index 7b3ddf8..8efd7c4 100644
--- a/packages/VpnServices/src/com/android/server/vpn/L2tpIpsecPskService.java
+++ b/packages/VpnServices/src/com/android/server/vpn/L2tpIpsecPskService.java
@@ -25,7 +25,7 @@
  * connection.
  */
 class L2tpIpsecPskService extends VpnService<L2tpIpsecPskProfile> {
-    private static final String IPSEC_DAEMON = "racoon";
+    private static final String IPSEC = "racoon";
 
     @Override
     protected void connect(String serverIp, String username, String password)
@@ -33,9 +33,9 @@
         L2tpIpsecPskProfile p = getProfile();
 
         // IPSEC
-        AndroidServiceProxy ipsecService = startService(IPSEC_DAEMON);
-        ipsecService.sendCommand(serverIp, L2tpService.L2TP_PORT,
-                p.getPresharedKey());
+        DaemonProxy ipsec = startDaemon(IPSEC);
+        ipsec.sendCommand(serverIp, L2tpService.L2TP_PORT, p.getPresharedKey());
+        ipsec.closeControlSocket();
 
         sleep(2000); // 2 seconds
 
diff --git a/packages/VpnServices/src/com/android/server/vpn/L2tpIpsecService.java b/packages/VpnServices/src/com/android/server/vpn/L2tpIpsecService.java
index e2d4ff4..56694b6 100644
--- a/packages/VpnServices/src/com/android/server/vpn/L2tpIpsecService.java
+++ b/packages/VpnServices/src/com/android/server/vpn/L2tpIpsecService.java
@@ -25,15 +25,16 @@
  * The service that manages the certificate based L2TP-over-IPSec VPN connection.
  */
 class L2tpIpsecService extends VpnService<L2tpIpsecProfile> {
-    private static final String IPSEC_DAEMON = "racoon";
+    private static final String IPSEC = "racoon";
 
     @Override
     protected void connect(String serverIp, String username, String password)
             throws IOException {
         // IPSEC
-        AndroidServiceProxy ipsecService = startService(IPSEC_DAEMON);
-        ipsecService.sendCommand(serverIp, L2tpService.L2TP_PORT,
+        DaemonProxy ipsec = startDaemon(IPSEC);
+        ipsec.sendCommand(serverIp, L2tpService.L2TP_PORT,
                 getUserkeyPath(), getUserCertPath(), getCaCertPath());
+        ipsec.closeControlSocket();
 
         sleep(2000); // 2 seconds
 
diff --git a/packages/VpnServices/src/com/android/server/vpn/MtpdHelper.java b/packages/VpnServices/src/com/android/server/vpn/MtpdHelper.java
index 5fac799..805a5b5 100644
--- a/packages/VpnServices/src/com/android/server/vpn/MtpdHelper.java
+++ b/packages/VpnServices/src/com/android/server/vpn/MtpdHelper.java
@@ -24,7 +24,7 @@
  * A helper class for sending commands to the MTP daemon (mtpd).
  */
 class MtpdHelper {
-    private static final String MTPD_SERVICE = "mtpd";
+    private static final String MTPD = "mtpd";
     private static final String VPN_LINKNAME = "vpn";
     private static final String PPP_ARGS_SEPARATOR = "";
 
@@ -37,7 +37,7 @@
         args.add(PPP_ARGS_SEPARATOR);
         addPppArguments(vpnService, args, serverIp, username, password);
 
-        AndroidServiceProxy mtpd = vpnService.startService(MTPD_SERVICE);
+        DaemonProxy mtpd = vpnService.startDaemon(MTPD);
         mtpd.sendCommand(args.toArray(new String[args.size()]));
     }
 
diff --git a/packages/VpnServices/src/com/android/server/vpn/NormalProcessProxy.java b/packages/VpnServices/src/com/android/server/vpn/NormalProcessProxy.java
deleted file mode 100644
index f0bbc34..0000000
--- a/packages/VpnServices/src/com/android/server/vpn/NormalProcessProxy.java
+++ /dev/null
@@ -1,85 +0,0 @@
-/*
- * Copyright (C) 2009, 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.android.server.vpn;
-
-import android.util.Log;
-
-import java.io.BufferedReader;
-import java.io.InputStreamReader;
-import java.io.IOException;
-
-/**
- * Proxy to perform a command with arguments.
- */
-public class NormalProcessProxy extends ProcessProxy {
-    private Process mProcess;
-    private String[] mArgs;
-    private String mTag;
-
-    /**
-     * Creates a proxy with the arguments.
-     * @param args the argument list with the first one being the command
-     */
-    public NormalProcessProxy(String ...args) {
-        if ((args == null) || (args.length == 0)) {
-            throw new IllegalArgumentException();
-        }
-        mArgs = args;
-        mTag = "PProxy_" + getName();
-    }
-
-    @Override
-    public String getName() {
-        return mArgs[0];
-    }
-
-    @Override
-    public synchronized void stop() {
-        if (isStopped()) return;
-        getHostThread().interrupt();
-        // TODO: not sure how to reliably kill a process
-        mProcess.destroy();
-        setState(ProcessState.STOPPING);
-    }
-
-    @Override
-    protected void performTask() throws IOException, InterruptedException {
-        String[] args = mArgs;
-        Log.d(mTag, "+++++  Execute: " + getEntireCommand());
-        ProcessBuilder pb = new ProcessBuilder(args);
-        setState(ProcessState.RUNNING);
-        Process p = mProcess = pb.start();
-        BufferedReader reader = new BufferedReader(new InputStreamReader(
-                p.getInputStream()));
-        while (true) {
-            String line = reader.readLine();
-            if ((line == null) || isStopping()) break;
-            Log.d(mTag, line);
-        }
-
-        Log.d(mTag, "-----  p.waitFor(): " + getName());
-        p.waitFor();
-        Log.d(mTag, "-----  Done: " + getName());
-    }
-
-    private CharSequence getEntireCommand() {
-        String[] args = mArgs;
-        StringBuilder sb = new StringBuilder(args[0]);
-        for (int i = 1; i < args.length; i++) sb.append(' ').append(args[i]);
-        return sb;
-    }
-}
diff --git a/packages/VpnServices/src/com/android/server/vpn/ProcessProxy.java b/packages/VpnServices/src/com/android/server/vpn/ProcessProxy.java
deleted file mode 100644
index 50fbf4b..0000000
--- a/packages/VpnServices/src/com/android/server/vpn/ProcessProxy.java
+++ /dev/null
@@ -1,210 +0,0 @@
-/*
- * Copyright (C) 2009, 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.android.server.vpn;
-
-import android.os.ConditionVariable;
-
-import java.io.IOException;
-
-/**
- * A proxy class that spawns a process to accomplish a certain task.
- */
-public abstract class ProcessProxy {
-    /**
-     * Defines the interface to call back when the process is finished or an
-     * error occurs during execution.
-     */
-    public static interface Callback {
-        /**
-         * Called when the process is finished.
-         * @param proxy the proxy that hosts the process
-         */
-        void done(ProcessProxy proxy);
-
-        /**
-         * Called when some error occurs.
-         * @param proxy the proxy that hosts the process
-         */
-        void error(ProcessProxy proxy, Throwable error);
-    }
-
-    protected enum ProcessState {
-        STOPPED, STARTING, RUNNING, STOPPING, ERROR
-    }
-
-    private ProcessState mState = ProcessState.STOPPED;
-    private ConditionVariable mLock = new ConditionVariable();
-    private Thread mThread;
-
-    /**
-     * Returns the name of the process.
-     */
-    public abstract String getName();
-
-    /**
-     * Starts the process with a callback.
-     * @param callback the callback to get notified when the process is finished
-     *      or an error occurs during execution
-     * @throws IOException when the process is already running or failed to
-     *      start
-     */
-    public synchronized void start(final Callback callback) throws IOException {
-        if (!isStopped()) {
-            throw new IOException("Process is already running: " + this);
-        }
-        mLock.close();
-        setState(ProcessState.STARTING);
-        Thread thread = new Thread(new Runnable() {
-            public void run() {
-                try {
-                    performTask();
-                    setState(ProcessState.STOPPED);
-                    mLock.open();
-                    if (callback != null) callback.done(ProcessProxy.this);
-                } catch (Throwable e) {
-                    setState(ProcessState.ERROR);
-                    if (callback != null) callback.error(ProcessProxy.this, e);
-                } finally {
-                    mThread = null;
-                }
-            }
-        });
-        thread.setPriority(Thread.MIN_PRIORITY);
-        thread.start();
-        mThread = thread;
-        if (!waitUntilRunning()) {
-            throw new IOException("Failed to start the process: " + this);
-        }
-    }
-
-    /**
-     * Starts the process.
-     * @throws IOException when the process is already running or failed to
-     *      start
-     */
-    public synchronized void start() throws IOException {
-        start(null);
-        if (!waitUntilDone()) {
-            throw new IOException("Failed to complete the process: " + this);
-        }
-    }
-
-    /**
-     * Returns the thread that hosts the process.
-     */
-    public Thread getHostThread() {
-        return mThread;
-    }
-
-    /**
-     * Blocks the current thread until the hosted process is finished.
-     *
-     * @return true if the process is finished normally; false if an error
-     *      occurs
-     */
-    public boolean waitUntilDone() {
-        while (!mLock.block(1000)) {
-            if (isStopped() || isInError()) break;
-        }
-        return isStopped();
-    }
-
-    /**
-     * Blocks the current thread until the hosted process is running.
-     *
-     * @return true if the process is running normally; false if the process
-     *      is in another state
-     */
-    private boolean waitUntilRunning() {
-        for (;;) {
-            if (!isStarting()) break;
-        }
-        return isRunning();
-    }
-
-    /**
-     * Stops and destroys the process.
-     */
-    public abstract void stop();
-
-    /**
-     * Checks whether the process is finished.
-     * @return true if the process is stopped
-     */
-    public boolean isStopped() {
-        return (mState == ProcessState.STOPPED);
-    }
-
-    /**
-     * Checks whether the process is being stopped.
-     * @return true if the process is being stopped
-     */
-    public boolean isStopping() {
-        return (mState == ProcessState.STOPPING);
-    }
-
-    /**
-     * Checks whether the process is being started.
-     * @return true if the process is being started
-     */
-    public boolean isStarting() {
-        return (mState == ProcessState.STARTING);
-    }
-
-    /**
-     * Checks whether the process is running.
-     * @return true if the process is running
-     */
-    public boolean isRunning() {
-        return (mState == ProcessState.RUNNING);
-    }
-
-    /**
-     * Checks whether some error has occurred and the process is stopped.
-     * @return true if some error has occurred and the process is stopped
-     */
-    public boolean isInError() {
-        return (mState == ProcessState.ERROR);
-    }
-
-    /**
-     * Performs the actual task. Subclasses must make sure that the method
-     * is blocked until the task is done or an error occurs.
-     */
-    protected abstract void performTask()
-            throws IOException, InterruptedException;
-
-    /**
-     * Sets the process state.
-     * @param state the new state to be in
-     */
-    protected void setState(ProcessState state) {
-        mState = state;
-    }
-
-    /**
-     * Makes the current thread sleep for the specified time.
-     * @param msec time to sleep in miliseconds
-     */
-    protected void sleep(int msec) {
-        try {
-            Thread.currentThread().sleep(msec);
-        } catch (InterruptedException e) {
-            throw new RuntimeException(e);
-        }
-    }
-}
diff --git a/packages/VpnServices/src/com/android/server/vpn/VpnService.java b/packages/VpnServices/src/com/android/server/vpn/VpnService.java
index 87bd780..60a07d5 100644
--- a/packages/VpnServices/src/com/android/server/vpn/VpnService.java
+++ b/packages/VpnServices/src/com/android/server/vpn/VpnService.java
@@ -50,14 +50,15 @@
     private static final String REMOTE_IP = "net.ipremote";
     private static final String DNS_DOMAIN_SUFFICES = "net.dns.search";
 
+    private static final int AUTH_ERROR_CODE = 51;
+
     private final String TAG = VpnService.class.getSimpleName();
 
     E mProfile;
     VpnServiceBinder mContext;
 
     private VpnState mState = VpnState.IDLE;
-    private boolean mInError;
-    private VpnConnectingError mError;
+    private Throwable mError;
 
     // connection settings
     private String mOriginalDns1;
@@ -68,8 +69,8 @@
 
     private long mStartTime; // VPN connection start time
 
-    // for helping managing multiple Android services
-    private ServiceHelper mServiceHelper = new ServiceHelper();
+    // for helping managing multiple daemons
+    private DaemonHelper mDaemonHelper = new DaemonHelper();
 
     // for helping showing, updating notification
     private NotificationHelper mNotification = new NotificationHelper();
@@ -81,18 +82,11 @@
             String password) throws IOException;
 
     /**
-     * Tears down the VPN connection. The base class simply terminates all the
-     * Android services. A subclass may need to do some clean-up before that.
+     * Starts a VPN daemon.
      */
-    protected void disconnect() {
-    }
-
-    /**
-     * Starts an Android service defined in init.rc.
-     */
-    protected AndroidServiceProxy startService(String serviceName)
+    protected DaemonProxy startDaemon(String daemonName)
             throws IOException {
-        return mServiceHelper.startService(serviceName);
+        return mDaemonHelper.startDaemon(daemonName);
     }
 
     /**
@@ -109,28 +103,6 @@
         return InetAddress.getByName(hostName).getHostAddress();
     }
 
-    /**
-     * Sets the system property. The method is blocked until the value is
-     * settled in.
-     * @param name the name of the property
-     * @param value the value of the property
-     * @throws IOException if it fails to set the property within 2 seconds
-     */
-    protected void setSystemProperty(String name, String value)
-            throws IOException {
-        SystemProperties.set(name, value);
-        for (int i = 0; i < 5; i++) {
-            String v = SystemProperties.get(name);
-            if (v.equals(value)) {
-                return;
-            } else {
-                Log.d(TAG, "sys_prop: wait for " + name + " to settle in");
-                sleep(400);
-            }
-        }
-        throw new IOException("Failed to set system property: " + name);
-    }
-
     void setContext(VpnServiceBinder context, E profile) {
         mContext = context;
         mProfile = profile;
@@ -153,44 +125,42 @@
             return true;
         } catch (Throwable e) {
             Log.e(TAG, "onConnect()", e);
-            mError = newConnectingError(e);
-            onError();
+            onError(e);
             return false;
         }
     }
 
-    synchronized void onDisconnect(boolean cleanUpServices) {
+    synchronized void onDisconnect() {
         try {
             Log.d(TAG, "       disconnecting VPN...");
             mState = VpnState.DISCONNECTING;
             broadcastConnectivity(VpnState.DISCONNECTING);
             mNotification.showDisconnect();
 
-            // subclass implementation
-            if (cleanUpServices) disconnect();
-
-            mServiceHelper.stop();
+            mDaemonHelper.stopAll();
         } catch (Throwable e) {
             Log.e(TAG, "onDisconnect()", e);
+        } finally {
             onFinalCleanUp();
         }
     }
 
-    synchronized void onError() {
+    private void onError(Throwable error) {
         // error may occur during or after connection setup
         // and it may be due to one or all services gone
-        mInError = true;
-        switch (mState) {
-        case CONNECTED:
-            onDisconnect(true);
-            break;
-
-        case CONNECTING:
-            onDisconnect(false);
-            break;
+        if (mError != null) {
+            Log.w(TAG, "   multiple errors occur, record the last one: "
+                    + error);
         }
+        mError = error;
+        onDisconnect();
     }
 
+    private void onError(int errorCode) {
+        onError(new VpnConnectingError(errorCode));
+    }
+
+
     private void onBeforeConnect() {
         mNotification.disableNotification();
 
@@ -201,41 +171,39 @@
     }
 
     private void waitUntilConnectedOrTimedout() {
-        // Run this in the background thread to not block UI
-        new Thread(new Runnable() {
-            public void run() {
-                sleep(2000); // 2 seconds
-                for (int i = 0; i < 60; i++) {
-                    if (VPN_IS_UP.equals(SystemProperties.get(VPN_STATUS))) {
-                        onConnected();
-                        return;
-                    } else if (mState != VpnState.CONNECTING) {
-                        break;
-                    }
-                    sleep(500); // 0.5 second
-                }
-
-                synchronized (VpnService.this) {
-                    if (mState == VpnState.CONNECTING) {
-                        Log.d(TAG, "       connecting timed out !!");
-                        mError = newConnectingError(
-                                new IOException("Connecting timed out"));
-                        onError();
-                    }
-                }
+        sleep(2000); // 2 seconds
+        for (int i = 0; i < 60; i++) {
+            if (mState != VpnState.CONNECTING) {
+                break;
+            } else if (VPN_IS_UP.equals(
+                    SystemProperties.get(VPN_STATUS))) {
+                onConnected();
+                return;
+            } else if (mDaemonHelper.anySocketError()) {
+                return;
             }
-        }).start();
+            sleep(500); // 0.5 second
+        }
+
+        synchronized (VpnService.this) {
+            if (mState == VpnState.CONNECTING) {
+                Log.d(TAG, "       connecting timed out !!");
+                onError(new IOException("Connecting timed out"));
+            }
+        }
     }
 
     private synchronized void onConnected() {
         Log.d(TAG, "onConnected()");
 
+        mDaemonHelper.closeSockets();
         saveVpnDnsProperties();
         saveAndSetDomainSuffices();
-        startConnectivityMonitor();
 
         mState = VpnState.CONNECTED;
         broadcastConnectivity(VpnState.CONNECTED);
+
+        enterConnectivityLoop();
     }
 
     private synchronized void onFinalCleanUp() {
@@ -244,7 +212,7 @@
         if (mState == VpnState.IDLE) return;
 
         // keep the notification when error occurs
-        if (!mInError) mNotification.disableNotification();
+        if (!anyError()) mNotification.disableNotification();
 
         restoreOriginalDnsProperties();
         restoreOriginalDomainSuffices();
@@ -255,37 +223,8 @@
         mContext.stopSelf();
     }
 
-    private VpnConnectingError newConnectingError(Throwable e) {
-        return new VpnConnectingError(
-                (e instanceof UnknownHostException)
-                ? VpnManager.VPN_ERROR_UNKNOWN_SERVER
-                : VpnManager.VPN_ERROR_CONNECTION_FAILED);
-    }
-
-    private synchronized void onOneServiceGone() {
-        switch (mState) {
-        case IDLE:
-        case DISCONNECTING:
-            break;
-
-        default:
-            onError();
-        }
-    }
-
-    private synchronized void onAllServicesGone() {
-        switch (mState) {
-        case IDLE:
-            break;
-
-        case DISCONNECTING:
-            // daemons are gone; now clean up everything
-            onFinalCleanUp();
-            break;
-
-        default:
-            onError();
-        }
+    private boolean anyError() {
+        return (mError != null);
     }
 
     private void restoreOriginalDnsProperties() {
@@ -341,46 +280,65 @@
 
     private void broadcastConnectivity(VpnState s) {
         VpnManager m = new VpnManager(mContext);
-        if ((s == VpnState.IDLE) && (mError != null)) {
-            m.broadcastConnectivity(mProfile.getName(), s,
-                    mError.getErrorCode());
+        Throwable err = mError;
+        if ((s == VpnState.IDLE) && (err != null)) {
+            if (err instanceof UnknownHostException) {
+                m.broadcastConnectivity(mProfile.getName(), s,
+                        VpnManager.VPN_ERROR_UNKNOWN_SERVER);
+            } else if (err instanceof VpnConnectingError) {
+                m.broadcastConnectivity(mProfile.getName(), s,
+                        ((VpnConnectingError) err).getErrorCode());
+            } else {
+                m.broadcastConnectivity(mProfile.getName(), s,
+                        VpnManager.VPN_ERROR_CONNECTION_FAILED);
+            }
         } else {
             m.broadcastConnectivity(mProfile.getName(), s);
         }
     }
 
-    private void startConnectivityMonitor() {
+    private void enterConnectivityLoop() {
         mStartTime = System.currentTimeMillis();
 
-        new Thread(new Runnable() {
-            public void run() {
-                Log.d(TAG, "   +++++   connectivity monitor running");
-                try {
-                    for (;;) {
-                        synchronized (VpnService.this) {
-                            if (mState != VpnState.CONNECTED) break;
-                            mNotification.update();
-                            checkConnectivity();
-                            VpnService.this.wait(1000); // 1 second
-                        }
-                    }
-                } catch (InterruptedException e) {
-                    Log.e(TAG, "connectivity monitor", e);
+        Log.d(TAG, "   +++++   connectivity monitor running");
+        try {
+            for (;;) {
+                synchronized (VpnService.this) {
+                    if (mState != VpnState.CONNECTED) break;
+                    mNotification.update();
+                    checkConnectivity();
+                    VpnService.this.wait(1000); // 1 second
                 }
-                Log.d(TAG, "   -----   connectivity monitor stopped");
             }
-        }).start();
+        } catch (InterruptedException e) {
+            Log.e(TAG, "connectivity monitor", e);
+        }
+        Log.d(TAG, "   -----   connectivity monitor stopped");
     }
 
     private void checkConnectivity() {
-        checkDnsProperties();
+        if (mDaemonHelper.anyDaemonStopped() || isLocalIpChanged()) {
+            onDisconnect();
+        }
     }
 
-    private void checkDnsProperties() {
+    private boolean isLocalIpChanged() {
+        // TODO
+        if (!isDnsIntact()) {
+            Log.w(TAG, "       local IP changed");
+            return true;
+        } else {
+            return false;
+        }
+    }
+
+    private boolean isDnsIntact() {
         String dns1 = SystemProperties.get(DNS1);
         if (!mVpnDns1.equals(dns1)) {
             Log.w(TAG, "   dns being overridden by: " + dns1);
-            onError();
+            return false;
+        } else {
+            return true;
         }
     }
 
@@ -391,56 +349,64 @@
         }
     }
 
-    private InetAddress toInetAddress(int addr) throws IOException {
-        byte[] aa = new byte[4];
-        for (int i= 0; i < aa.length; i++) {
-            aa[i] = (byte) (addr & 0x0FF);
-            addr >>= 8;
-        }
-        return InetAddress.getByAddress(aa);
-    }
+    private class DaemonHelper {
+        private List<DaemonProxy> mDaemonList =
+                new ArrayList<DaemonProxy>();
 
-    private class ServiceHelper implements ProcessProxy.Callback {
-        private List<AndroidServiceProxy> mServiceList =
-                new ArrayList<AndroidServiceProxy>();
-
-        // starts an Android service
-        AndroidServiceProxy startService(String serviceName)
+        synchronized DaemonProxy startDaemon(String daemonName)
                 throws IOException {
-            AndroidServiceProxy service = new AndroidServiceProxy(serviceName);
-            mServiceList.add(service);
-            service.start(this);
-            return service;
+            DaemonProxy daemon = new DaemonProxy(daemonName);
+            mDaemonList.add(daemon);
+            daemon.start();
+            return daemon;
         }
 
-        // stops all the Android services
-        void stop() {
-            if (mServiceList.isEmpty()) {
+        synchronized void stopAll() {
+            if (mDaemonList.isEmpty()) {
                 onFinalCleanUp();
             } else {
-                for (AndroidServiceProxy s : mServiceList) s.stop();
+                for (DaemonProxy s : mDaemonList) s.stop();
             }
         }
 
-        //@Override
-        public void done(ProcessProxy p) {
-            Log.d(TAG, "service done: " + p.getName());
-            commonCallback((AndroidServiceProxy) p);
+        synchronized void closeSockets() {
+            for (DaemonProxy s : mDaemonList) s.closeControlSocket();
         }
 
-        //@Override
-        public void error(ProcessProxy p, Throwable e) {
-            Log.e(TAG, "service error: " + p.getName(), e);
-            if (e instanceof VpnConnectingError) {
-                mError = (VpnConnectingError) e;
+        synchronized boolean anyDaemonStopped() {
+            for (DaemonProxy s : mDaemonList) {
+                if (s.isStopped()) {
+                    Log.w(TAG, "       daemon gone: " + s.getName());
+                    return true;
+                }
             }
-            commonCallback((AndroidServiceProxy) p);
+            return false;
         }
 
-        private void commonCallback(AndroidServiceProxy service) {
-            mServiceList.remove(service);
-            onOneServiceGone();
-            if (mServiceList.isEmpty()) onAllServicesGone();
+        private int getResultFromSocket(DaemonProxy s) {
+            try {
+                return s.getResultFromSocket();
+            } catch (IOException e) {
+                return -1;
+            }
+        }
+
+        synchronized boolean anySocketError() {
+            for (DaemonProxy s : mDaemonList) {
+                switch (getResultFromSocket(s)) {
+                    case 0:
+                        continue;
+
+                    case AUTH_ERROR_CODE:
+                        onError(VpnManager.VPN_ERROR_AUTH);
+                        return true;
+
+                    default:
+                        onError(VpnManager.VPN_ERROR_CONNECTION_FAILED);
+                        return true;
+                }
+            }
+            return false;
         }
     }
 
diff --git a/packages/VpnServices/src/com/android/server/vpn/VpnServiceBinder.java b/packages/VpnServices/src/com/android/server/vpn/VpnServiceBinder.java
index 32b8e51..513a2c9 100644
--- a/packages/VpnServices/src/com/android/server/vpn/VpnServiceBinder.java
+++ b/packages/VpnServices/src/com/android/server/vpn/VpnServiceBinder.java
@@ -42,11 +42,13 @@
 
     private final IBinder mBinder = new IVpnService.Stub() {
         public boolean connect(VpnProfile p, String username, String password) {
+            android.util.Log.d("VpnServiceBinder", "becoming foreground");
+            setForeground(true);
             return VpnServiceBinder.this.connect(p, username, password);
         }
 
         public void disconnect() {
-            if (mService != null) mService.onDisconnect(true);
+            VpnServiceBinder.this.disconnect();
         }
 
         public void checkStatus(VpnProfile p) {
@@ -54,21 +56,39 @@
         }
     };
 
-    public void onStart (Intent intent, int startId) {
+    @Override
+    public void onStart(Intent intent, int startId) {
         super.onStart(intent, startId);
-        setForeground(true);
-        android.util.Log.d("VpnServiceBinder", "becomes a foreground service");
     }
 
+    @Override
     public IBinder onBind(Intent intent) {
         return mBinder;
     }
 
-    private synchronized boolean connect(
-            VpnProfile p, String username, String password) {
+    private synchronized boolean connect(final VpnProfile p,
+            final String username, final String password) {
         if (mService != null) return false;
-        mService = createService(p);
-        return mService.onConnect(username, password);
+
+        new Thread(new Runnable() {
+            public void run() {
+                mService = createService(p);
+                mService.onConnect(username, password);
+            }
+        }).start();
+        return true;
+    }
+
+    private synchronized void disconnect() {
+        if (mService == null) return;
+
+        new Thread(new Runnable() {
+            public void run() {
+                mService.onDisconnect();
+                android.util.Log.d("VpnServiceBinder", "becoming background");
+                setForeground(false);
+            }
+        }).start();
     }
 
     private synchronized void checkStatus(VpnProfile p) {