Begin moving VPN to NetworkStateTracker pattern.

Created base tracker that handles common bookkeeping, and move VPN
to become a tracker.  VPN status is now reflected in NetworkInfo, and
is mapped to LegacyVpnInfo.

Legacy VPN now "babysits" any init services it starts, watching for
when they stop unexpectedly.

Bug: 5756357
Change-Id: Iba7ec79da69469f6bd9a970cc39cf6b885b4c9c4
diff --git a/services/java/com/android/server/ConnectivityService.java b/services/java/com/android/server/ConnectivityService.java
index cb6ce4b..d0db0d2 100644
--- a/services/java/com/android/server/ConnectivityService.java
+++ b/services/java/com/android/server/ConnectivityService.java
@@ -115,13 +115,15 @@
  * @hide
  */
 public class ConnectivityService extends IConnectivityManager.Stub {
+    private static final String TAG = "ConnectivityService";
 
     private static final boolean DBG = true;
     private static final boolean VDBG = false;
-    private static final String TAG = "ConnectivityService";
 
     private static final boolean LOGD_RULES = false;
 
+    // TODO: create better separation between radio types and network types
+
     // how long to wait before switching back to a radio's default network
     private static final int RESTORE_DEFAULT_NETWORK_DELAY = 1 * 60 * 1000;
     // system property that can override the above value
@@ -136,6 +138,7 @@
     private boolean mTetheringConfigValid = false;
 
     private Vpn mVpn;
+    private VpnCallback mVpnCallback = new VpnCallback();
 
     /** Lock around {@link #mUidRules} and {@link #mMeteredIfaces}. */
     private Object mRulesLock = new Object();
@@ -328,7 +331,7 @@
         this(context, netd, statsService, policyManager, null);
     }
 
-    public ConnectivityService(Context context, INetworkManagementService netd,
+    public ConnectivityService(Context context, INetworkManagementService netManager,
             INetworkStatsService statsService, INetworkPolicyManager policyManager,
             NetworkFactory netFactory) {
         if (DBG) log("ConnectivityService starting up");
@@ -366,7 +369,7 @@
         }
 
         mContext = checkNotNull(context, "missing Context");
-        mNetd = checkNotNull(netd, "missing INetworkManagementService");
+        mNetd = checkNotNull(netManager, "missing INetworkManagementService");
         mPolicyManager = checkNotNull(policyManager, "missing INetworkPolicyManager");
 
         try {
@@ -506,11 +509,11 @@
                                   mTethering.getTetherableBluetoothRegexs().length != 0) &&
                                  mTethering.getUpstreamIfaceTypes().length != 0);
 
-        mVpn = new Vpn(mContext, new VpnCallback());
+        mVpn = new Vpn(mContext, mVpnCallback, mNetd);
+        mVpn.startMonitoring(mContext, mTrackerHandler);
 
         try {
             mNetd.registerObserver(mTethering);
-            mNetd.registerObserver(mVpn);
             mNetd.registerObserver(mDataActivityObserver);
         } catch (RemoteException e) {
             loge("Error registering observer :" + e);
@@ -2238,9 +2241,9 @@
      */
    public void updateNetworkSettings(NetworkStateTracker nt) {
         String key = nt.getTcpBufferSizesPropName();
-        String bufferSizes = SystemProperties.get(key);
+        String bufferSizes = key == null ? null : SystemProperties.get(key);
 
-        if (bufferSizes.length() == 0) {
+        if (TextUtils.isEmpty(bufferSizes)) {
             if (VDBG) log(key + " not found in system properties. Using defaults");
 
             // Setting to default values so we won't be stuck to previous values
@@ -3153,10 +3156,14 @@
      * be done whenever a better abstraction is developed.
      */
     public class VpnCallback {
-
         private VpnCallback() {
         }
 
+        public void onStateChanged(NetworkInfo info) {
+            // TODO: if connected, release delayed broadcast
+            // TODO: if disconnected, consider kicking off reconnect
+        }
+
         public void override(List<String> dnsServers, List<String> searchDomains) {
             if (dnsServers == null) {
                 restore();
diff --git a/services/java/com/android/server/connectivity/Vpn.java b/services/java/com/android/server/connectivity/Vpn.java
index 1232846..d490f24 100644
--- a/services/java/com/android/server/connectivity/Vpn.java
+++ b/services/java/com/android/server/connectivity/Vpn.java
@@ -16,8 +16,11 @@
 
 package com.android.server.connectivity;
 
+import static android.Manifest.permission.BIND_VPN_SERVICE;
+
 import android.app.Notification;
 import android.app.NotificationManager;
+import android.app.PendingIntent;
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
@@ -28,15 +31,21 @@
 import android.graphics.Bitmap;
 import android.graphics.Canvas;
 import android.graphics.drawable.Drawable;
+import android.net.BaseNetworkStateTracker;
+import android.net.ConnectivityManager;
 import android.net.INetworkManagementEventObserver;
 import android.net.LocalSocket;
 import android.net.LocalSocketAddress;
+import android.net.NetworkInfo;
+import android.net.NetworkInfo.DetailedState;
 import android.os.Binder;
 import android.os.FileUtils;
 import android.os.IBinder;
+import android.os.INetworkManagementService;
 import android.os.Parcel;
 import android.os.ParcelFileDescriptor;
 import android.os.Process;
+import android.os.RemoteException;
 import android.os.SystemClock;
 import android.os.SystemService;
 import android.util.Log;
@@ -44,7 +53,9 @@
 import com.android.internal.R;
 import com.android.internal.net.LegacyVpnInfo;
 import com.android.internal.net.VpnConfig;
+import com.android.internal.util.Preconditions;
 import com.android.server.ConnectivityService.VpnCallback;
+import com.android.server.net.BaseNetworkObserver;
 
 import java.io.File;
 import java.io.InputStream;
@@ -57,24 +68,63 @@
 /**
  * @hide
  */
-public class Vpn extends INetworkManagementEventObserver.Stub {
+public class Vpn extends BaseNetworkStateTracker {
+    private static final String TAG = "Vpn";
+    private static final boolean LOGD = true;
+    
+    // TODO: create separate trackers for each unique VPN to support
+    // automated reconnection
 
-    private final static String TAG = "Vpn";
-
-    private final static String BIND_VPN_SERVICE =
-            android.Manifest.permission.BIND_VPN_SERVICE;
-
-    private final Context mContext;
     private final VpnCallback mCallback;
 
     private String mPackage = VpnConfig.LEGACY_VPN;
     private String mInterface;
     private Connection mConnection;
     private LegacyVpnRunner mLegacyVpnRunner;
+    private PendingIntent mStatusIntent;
 
-    public Vpn(Context context, VpnCallback callback) {
+    public Vpn(Context context, VpnCallback callback, INetworkManagementService netService) {
+        // TODO: create dedicated TYPE_VPN network type
+        super(ConnectivityManager.TYPE_DUMMY);
         mContext = context;
         mCallback = callback;
+
+        try {
+            netService.registerObserver(mObserver);
+        } catch (RemoteException e) {
+            Log.wtf(TAG, "Problem registering observer", e);
+        }
+    }
+
+    @Override
+    protected void startMonitoringInternal() {
+        // Ignored; events are sent through callbacks for now
+    }
+
+    @Override
+    public boolean teardown() {
+        // TODO: finish migration to unique tracker for each VPN
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public boolean reconnect() {
+        // TODO: finish migration to unique tracker for each VPN
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public String getTcpBufferSizesPropName() {
+        return PROP_TCP_BUFFER_UNKNOWN;
+    }
+
+    /**
+     * Update current state, dispaching event to listeners.
+     */
+    private void updateState(DetailedState detailedState, String reason) {
+        if (LOGD) Log.d(TAG, "setting state=" + detailedState + ", reason=" + reason);
+        mNetworkInfo.setDetailedState(detailedState, reason, null);
+        mCallback.onStateChanged(new NetworkInfo(mNetworkInfo));
     }
 
     /**
@@ -113,10 +163,13 @@
         // Reset the interface and hide the notification.
         if (mInterface != null) {
             jniReset(mInterface);
-            long identity = Binder.clearCallingIdentity();
-            mCallback.restore();
-            hideNotification();
-            Binder.restoreCallingIdentity(identity);
+            final long token = Binder.clearCallingIdentity();
+            try {
+                mCallback.restore();
+                hideNotification();
+            } finally {
+                Binder.restoreCallingIdentity(token);
+            }
             mInterface = null;
         }
 
@@ -137,6 +190,7 @@
 
         Log.i(TAG, "Switched from " + mPackage + " to " + newPackage);
         mPackage = newPackage;
+        updateState(DetailedState.IDLE, "prepare");
         return true;
     }
 
@@ -145,7 +199,7 @@
      * interface. The socket is NOT closed by this method.
      *
      * @param socket The socket to be bound.
-     * @param name The name of the interface.
+     * @param interfaze The name of the interface.
      */
     public void protect(ParcelFileDescriptor socket, String interfaze) throws Exception {
         PackageManager pm = mContext.getPackageManager();
@@ -209,6 +263,7 @@
         // Configure the interface. Abort if any of these steps fails.
         ParcelFileDescriptor tun = ParcelFileDescriptor.adoptFd(jniCreate(config.mtu));
         try {
+            updateState(DetailedState.CONNECTING, "establish");
             String interfaze = jniGetName(tun.getFd());
             if (jniSetAddresses(interfaze, config.addresses) < 1) {
                 throw new IllegalArgumentException("At least one address must be specified");
@@ -229,6 +284,7 @@
             mConnection = connection;
             mInterface = interfaze;
         } catch (RuntimeException e) {
+            updateState(DetailedState.FAILED, "establish");
             IoUtils.closeQuietly(tun);
             throw e;
         }
@@ -239,57 +295,61 @@
         config.interfaze = mInterface;
 
         // Override DNS servers and show the notification.
-        long identity = Binder.clearCallingIdentity();
-        mCallback.override(config.dnsServers, config.searchDomains);
-        showNotification(config, label, bitmap);
-        Binder.restoreCallingIdentity(identity);
+        final long token = Binder.clearCallingIdentity();
+        try {
+            mCallback.override(config.dnsServers, config.searchDomains);
+            showNotification(config, label, bitmap);
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+        // TODO: ensure that contract class eventually marks as connected
+        updateState(DetailedState.AUTHENTICATING, "establish");
         return tun;
     }
 
-    // INetworkManagementEventObserver.Stub
-    @Override
-    public void interfaceAdded(String interfaze) {
-    }
-
-    // INetworkManagementEventObserver.Stub
-    @Override
-    public synchronized void interfaceStatusChanged(String interfaze, boolean up) {
-        if (!up && mLegacyVpnRunner != null) {
-            mLegacyVpnRunner.check(interfaze);
+    @Deprecated
+    public synchronized void interfaceStatusChanged(String iface, boolean up) {
+        try {
+            mObserver.interfaceStatusChanged(iface, up);
+        } catch (RemoteException e) {
+            // ignored; target is local
         }
     }
 
-    // INetworkManagementEventObserver.Stub
-    @Override
-    public void interfaceLinkStateChanged(String interfaze, boolean up) {
-    }
-
-    // INetworkManagementEventObserver.Stub
-    @Override
-    public synchronized void interfaceRemoved(String interfaze) {
-        if (interfaze.equals(mInterface) && jniCheck(interfaze) == 0) {
-            long identity = Binder.clearCallingIdentity();
-            mCallback.restore();
-            hideNotification();
-            Binder.restoreCallingIdentity(identity);
-            mInterface = null;
-            if (mConnection != null) {
-                mContext.unbindService(mConnection);
-                mConnection = null;
-            } else if (mLegacyVpnRunner != null) {
-                mLegacyVpnRunner.exit();
-                mLegacyVpnRunner = null;
+    private INetworkManagementEventObserver mObserver = new BaseNetworkObserver() {
+        @Override
+        public void interfaceStatusChanged(String interfaze, boolean up) {
+            synchronized (Vpn.this) {
+                if (!up && mLegacyVpnRunner != null) {
+                    mLegacyVpnRunner.check(interfaze);
+                }
             }
         }
-    }
 
-    // INetworkManagementEventObserver.Stub
-    @Override
-    public void limitReached(String limit, String interfaze) {
-    }
-
-    public void interfaceClassDataActivityChanged(String label, boolean active) {
-    }
+        @Override
+        public void interfaceRemoved(String interfaze) {
+            synchronized (Vpn.this) {
+                if (interfaze.equals(mInterface) && jniCheck(interfaze) == 0) {
+                    final long token = Binder.clearCallingIdentity();
+                    try {
+                        mCallback.restore();
+                        hideNotification();
+                    } finally {
+                        Binder.restoreCallingIdentity(token);
+                    }
+                    mInterface = null;
+                    if (mConnection != null) {
+                        mContext.unbindService(mConnection);
+                        mConnection = null;
+                        updateState(DetailedState.DISCONNECTED, "interfaceRemoved");
+                    } else if (mLegacyVpnRunner != null) {
+                        mLegacyVpnRunner.exit();
+                        mLegacyVpnRunner = null;
+                    }
+                }
+            }
+        }
+    };
 
     private void enforceControlPermission() {
         // System user is allowed to control VPN.
@@ -326,6 +386,8 @@
     }
 
     private void showNotification(VpnConfig config, String label, Bitmap icon) {
+        mStatusIntent = VpnConfig.getIntentForStatusPanel(mContext, config);
+
         NotificationManager nm = (NotificationManager)
                 mContext.getSystemService(Context.NOTIFICATION_SERVICE);
 
@@ -341,15 +403,17 @@
                     .setLargeIcon(icon)
                     .setContentTitle(title)
                     .setContentText(text)
-                    .setContentIntent(VpnConfig.getIntentForStatusPanel(mContext, config))
+                    .setContentIntent(mStatusIntent)
                     .setDefaults(0)
                     .setOngoing(true)
-                    .getNotification();
+                    .build();
             nm.notify(R.drawable.vpn_connected, notification);
         }
     }
 
     private void hideNotification() {
+        mStatusIntent = null;
+
         NotificationManager nm = (NotificationManager)
                 mContext.getSystemService(Context.NOTIFICATION_SERVICE);
 
@@ -372,25 +436,51 @@
      * thread, so callers will not be blocked for a long time.
      *
      * @param config The parameters to configure the network.
-     * @param raoocn The arguments to be passed to racoon.
+     * @param racoon The arguments to be passed to racoon.
      * @param mtpd The arguments to be passed to mtpd.
      */
     public synchronized void startLegacyVpn(VpnConfig config, String[] racoon, String[] mtpd) {
+        stopLegacyVpn();
+
+        // TODO: move legacy definition to settings
+        config.legacy = true;
+
         // Prepare for the new request. This also checks the caller.
         prepare(null, VpnConfig.LEGACY_VPN);
+        updateState(DetailedState.CONNECTING, "startLegacyVpn");
 
         // Start a new LegacyVpnRunner and we are done!
         mLegacyVpnRunner = new LegacyVpnRunner(config, racoon, mtpd);
         mLegacyVpnRunner.start();
     }
 
+    public synchronized void stopLegacyVpn() {
+        if (mLegacyVpnRunner != null) {
+            mLegacyVpnRunner.exit();
+            mLegacyVpnRunner = null;
+
+            synchronized (LegacyVpnRunner.TAG) {
+                // wait for old thread to completely finish before spinning up
+                // new instance, otherwise state updates can be out of order.
+            }
+        }
+    }
+
     /**
      * Return the information of the current ongoing legacy VPN.
      */
     public synchronized LegacyVpnInfo getLegacyVpnInfo() {
         // Check if the caller is authorized.
         enforceControlPermission();
-        return (mLegacyVpnRunner == null) ? null : mLegacyVpnRunner.getInfo();
+        if (mLegacyVpnRunner == null) return null;
+
+        final LegacyVpnInfo info = new LegacyVpnInfo();
+        info.key = mLegacyVpnRunner.mConfig.user;
+        info.state = LegacyVpnInfo.stateFromNetworkInfo(mNetworkInfo);
+        if (mNetworkInfo.isConnected()) {
+            info.intent = mStatusIntent;
+        }
+        return info;
     }
 
     /**
@@ -407,8 +497,6 @@
         private final String[] mDaemons;
         private final String[][] mArguments;
         private final LocalSocket[] mSockets;
-        private final String mOuterInterface;
-        private final LegacyVpnInfo mInfo;
 
         private long mTimer = -1;
 
@@ -416,20 +504,13 @@
             super(TAG);
             mConfig = config;
             mDaemons = new String[] {"racoon", "mtpd"};
+            // TODO: clear arguments from memory once launched
             mArguments = new String[][] {racoon, mtpd};
             mSockets = new LocalSocket[mDaemons.length];
-            mInfo = new LegacyVpnInfo();
-
-            // This is the interface which VPN is running on.
-            mOuterInterface = mConfig.interfaze;
-
-            // Legacy VPN is not a real package, so we use it to carry the key.
-            mInfo.key = mConfig.user;
-            mConfig.user = VpnConfig.LEGACY_VPN;
         }
 
         public void check(String interfaze) {
-            if (interfaze.equals(mOuterInterface)) {
+            if (interfaze.equals(mConfig.interfaze)) {
                 Log.i(TAG, "Legacy VPN is going down with " + interfaze);
                 exit();
             }
@@ -441,15 +522,7 @@
             for (LocalSocket socket : mSockets) {
                 IoUtils.closeQuietly(socket);
             }
-        }
-
-        public LegacyVpnInfo getInfo() {
-            // Update the info when VPN is disconnected.
-            if (mInfo.state == LegacyVpnInfo.STATE_CONNECTED && mInterface == null) {
-                mInfo.state = LegacyVpnInfo.STATE_DISCONNECTED;
-                mInfo.intent = null;
-            }
-            return mInfo;
+            updateState(DetailedState.DISCONNECTED, "exit");
         }
 
         @Override
@@ -459,6 +532,7 @@
             synchronized (TAG) {
                 Log.v(TAG, "Executing");
                 execute();
+                monitorDaemons();
             }
         }
 
@@ -470,17 +544,17 @@
             } else if (now - mTimer <= 60000) {
                 Thread.sleep(yield ? 200 : 1);
             } else {
-                mInfo.state = LegacyVpnInfo.STATE_TIMEOUT;
+                updateState(DetailedState.FAILED, "checkpoint");
                 throw new IllegalStateException("Time is up");
             }
         }
 
         private void execute() {
             // Catch all exceptions so we can clean up few things.
+            boolean initFinished = false;
             try {
                 // Initialize the timer.
                 checkpoint(false);
-                mInfo.state = LegacyVpnInfo.STATE_INITIALIZING;
 
                 // Wait for the daemons to stop.
                 for (String daemon : mDaemons) {
@@ -496,6 +570,7 @@
                     throw new IllegalStateException("Cannot delete the state");
                 }
                 new File("/data/misc/vpn/abort").delete();
+                initFinished = true;
 
                 // Check if we need to restart any of the daemons.
                 boolean restart = false;
@@ -503,10 +578,10 @@
                     restart = restart || (arguments != null);
                 }
                 if (!restart) {
-                    mInfo.state = LegacyVpnInfo.STATE_DISCONNECTED;
+                    updateState(DetailedState.DISCONNECTED, "execute");
                     return;
                 }
-                mInfo.state = LegacyVpnInfo.STATE_CONNECTING;
+                updateState(DetailedState.CONNECTING, "execute");
 
                 // Start the daemon with arguments.
                 for (int i = 0; i < mDaemons.length; ++i) {
@@ -633,26 +708,53 @@
                     showNotification(mConfig, null, null);
 
                     Log.i(TAG, "Connected!");
-                    mInfo.state = LegacyVpnInfo.STATE_CONNECTED;
-                    mInfo.intent = VpnConfig.getIntentForStatusPanel(mContext, null);
+                    updateState(DetailedState.CONNECTED, "execute");
                 }
             } catch (Exception e) {
                 Log.i(TAG, "Aborting", e);
                 exit();
             } finally {
                 // Kill the daemons if they fail to stop.
-                if (mInfo.state == LegacyVpnInfo.STATE_INITIALIZING) {
+                if (!initFinished) {
                     for (String daemon : mDaemons) {
                         SystemService.stop(daemon);
                     }
                 }
 
                 // Do not leave an unstable state.
-                if (mInfo.state == LegacyVpnInfo.STATE_INITIALIZING ||
-                        mInfo.state == LegacyVpnInfo.STATE_CONNECTING) {
-                    mInfo.state = LegacyVpnInfo.STATE_FAILED;
+                if (!initFinished || mNetworkInfo.getDetailedState() == DetailedState.CONNECTING) {
+                    updateState(DetailedState.FAILED, "execute");
                 }
             }
         }
+
+        /**
+         * Monitor the daemons we started, moving to disconnected state if the
+         * underlying services fail.
+         */
+        private void monitorDaemons() {
+            if (!mNetworkInfo.isConnected()) {
+                return;
+            }
+
+            try {
+                while (true) {
+                    Thread.sleep(2000);
+                    for (int i = 0; i < mDaemons.length; i++) {
+                        if (mArguments[i] != null && SystemService.isStopped(mDaemons[i])) {
+                            return;
+                        }
+                    }
+                }
+            } catch (InterruptedException e) {
+                Log.d(TAG, "interrupted during monitorDaemons(); stopping services");
+            } finally {
+                for (String daemon : mDaemons) {
+                    SystemService.stop(daemon);
+                }
+
+                updateState(DetailedState.DISCONNECTED, "babysit");
+            }
+        }
     }
 }