am 0e90f738: am cd2a20f2: am 5a2772e2: Merge "Fix code problem in writePidDns"

* commit '0e90f738aa82d9b30e6b4f43cd321e3f9cad2fc1':
  Fix code problem in writePidDns
diff --git a/core/java/android/net/ConnectivityManager.java b/core/java/android/net/ConnectivityManager.java
index 5f8793c..a570473 100644
--- a/core/java/android/net/ConnectivityManager.java
+++ b/core/java/android/net/ConnectivityManager.java
@@ -89,11 +89,21 @@
      *             should always obtain network information through
      *             {@link #getActiveNetworkInfo()} or
      *             {@link #getAllNetworkInfo()}.
+     * @see #EXTRA_NETWORK_TYPE
      */
     @Deprecated
     public static final String EXTRA_NETWORK_INFO = "networkInfo";
 
     /**
+     * Network type which triggered a {@link #CONNECTIVITY_ACTION} broadcast.
+     * Can be used with {@link #getNetworkInfo(int)} to get {@link NetworkInfo}
+     * state based on the calling application.
+     *
+     * @see android.content.Intent#getIntExtra(String, int)
+     */
+    public static final String EXTRA_NETWORK_TYPE = "networkType";
+
+    /**
      * The lookup key for a boolean that indicates whether a connect event
      * is for a network to which the connectivity manager was failing over
      * following a disconnect on another network.
@@ -137,6 +147,28 @@
     public static final String EXTRA_INET_CONDITION = "inetCondition";
 
     /**
+     * Broadcast action to indicate the change of data activity status
+     * (idle or active) on a network in a recent period.
+     * The network becomes active when data transimission is started, or
+     * idle if there is no data transimition for a period of time.
+     * {@hide}
+     */
+    @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+    public static final String ACTION_DATA_ACTIVITY_CHANGE = "android.net.conn.DATA_ACTIVITY_CHANGE";
+    /**
+     * The lookup key for an enum that indicates the network device type on which this data activity
+     * change happens.
+     * {@hide}
+     */
+    public static final String EXTRA_DEVICE_TYPE = "deviceType";
+    /**
+     * The lookup key for a boolean that indicates the device is active or not. {@code true} means
+     * it is actively sending or receiving data and {@code false} means it is idle.
+     * {@hide}
+     */
+    public static final String EXTRA_IS_ACTIVE = "isActive";
+
+    /**
      * Broadcast Action: The setting for background data usage has changed
      * values. Use {@link #getBackgroundDataSetting()} to get the current value.
      * <p>
@@ -880,4 +912,24 @@
             return false;
         }
     }
+
+    /** {@hide} */
+    public boolean updateLockdownVpn() {
+        try {
+            return mService.updateLockdownVpn();
+        } catch (RemoteException e) {
+            return false;
+        }
+    }
+
+    /**
+     * {@hide}
+     */
+    public void captivePortalCheckComplete(NetworkInfo info) {
+        try {
+            mService.captivePortalCheckComplete(info);
+        } catch (RemoteException e) {
+        }
+    }
+
 }
diff --git a/core/java/android/net/IConnectivityManager.aidl b/core/java/android/net/IConnectivityManager.aidl
index 92aeff2..056fa03 100644
--- a/core/java/android/net/IConnectivityManager.aidl
+++ b/core/java/android/net/IConnectivityManager.aidl
@@ -26,6 +26,7 @@
 
 import com.android.internal.net.LegacyVpnInfo;
 import com.android.internal.net.VpnConfig;
+import com.android.internal.net.VpnProfile;
 
 /**
  * Interface that answers queries about, and allows changing, the
@@ -118,7 +119,11 @@
 
     ParcelFileDescriptor establishVpn(in VpnConfig config);
 
-    void startLegacyVpn(in VpnConfig config, in String[] racoon, in String[] mtpd);
+    void startLegacyVpn(in VpnProfile profile);
 
     LegacyVpnInfo getLegacyVpnInfo();
+
+    boolean updateLockdownVpn();
+
+    void captivePortalCheckComplete(in NetworkInfo info);
 }
diff --git a/core/java/android/net/NetworkInfo.java b/core/java/android/net/NetworkInfo.java
index 0bc6b58..0b23cb7 100644
--- a/core/java/android/net/NetworkInfo.java
+++ b/core/java/android/net/NetworkInfo.java
@@ -79,7 +79,9 @@
         /** Access to this network is blocked. */
         BLOCKED,
         /** Link has poor connectivity. */
-        VERIFYING_POOR_LINK
+        VERIFYING_POOR_LINK,
+        /** Checking if network is a captive portal */
+        CAPTIVE_PORTAL_CHECK,
     }
 
     /**
@@ -97,6 +99,7 @@
         stateMap.put(DetailedState.AUTHENTICATING, State.CONNECTING);
         stateMap.put(DetailedState.OBTAINING_IPADDR, State.CONNECTING);
         stateMap.put(DetailedState.VERIFYING_POOR_LINK, State.CONNECTING);
+        stateMap.put(DetailedState.CAPTIVE_PORTAL_CHECK, State.CONNECTING);
         stateMap.put(DetailedState.CONNECTED, State.CONNECTED);
         stateMap.put(DetailedState.SUSPENDED, State.SUSPENDED);
         stateMap.put(DetailedState.DISCONNECTING, State.DISCONNECTING);
diff --git a/services/java/com/android/server/ConnectivityService.java b/services/java/com/android/server/ConnectivityService.java
index 86ada40..04991bb 100644
--- a/services/java/com/android/server/ConnectivityService.java
+++ b/services/java/com/android/server/ConnectivityService.java
@@ -16,25 +16,38 @@
 
 package com.android.server;
 
+import static android.Manifest.permission.CONNECTIVITY_INTERNAL;
 import static android.Manifest.permission.MANAGE_NETWORK_POLICY;
+import static android.Manifest.permission.RECEIVE_DATA_ACTIVITY_CHANGE;
 import static android.net.ConnectivityManager.CONNECTIVITY_ACTION;
 import static android.net.ConnectivityManager.CONNECTIVITY_ACTION_IMMEDIATE;
+import static android.net.ConnectivityManager.TYPE_BLUETOOTH;
+import static android.net.ConnectivityManager.TYPE_DUMMY;
+import static android.net.ConnectivityManager.TYPE_ETHERNET;
+import static android.net.ConnectivityManager.TYPE_MOBILE;
+import static android.net.ConnectivityManager.TYPE_WIFI;
+import static android.net.ConnectivityManager.TYPE_WIMAX;
+import static android.net.ConnectivityManager.getNetworkTypeName;
 import static android.net.ConnectivityManager.isNetworkTypeValid;
 import static android.net.NetworkPolicyManager.RULE_ALLOW_ALL;
 import static android.net.NetworkPolicyManager.RULE_REJECT_METERED;
 
 import android.bluetooth.BluetoothTetheringDataTracker;
+import android.content.BroadcastReceiver;
 import android.content.ContentResolver;
 import android.content.Context;
 import android.content.ContextWrapper;
 import android.content.Intent;
+import android.content.IntentFilter;
 import android.content.pm.PackageManager;
 import android.content.res.Resources;
 import android.database.ContentObserver;
+import android.net.CaptivePortalTracker;
 import android.net.ConnectivityManager;
 import android.net.DummyDataStateTracker;
 import android.net.EthernetDataTracker;
 import android.net.IConnectivityManager;
+import android.net.INetworkManagementEventObserver;
 import android.net.INetworkPolicyListener;
 import android.net.INetworkPolicyManager;
 import android.net.INetworkStatsService;
@@ -68,7 +81,10 @@
 import android.os.ServiceManager;
 import android.os.SystemClock;
 import android.os.SystemProperties;
+import android.os.UserHandle;
 import android.provider.Settings;
+import android.security.Credentials;
+import android.security.KeyStore;
 import android.text.TextUtils;
 import android.util.EventLog;
 import android.util.Slog;
@@ -76,21 +92,23 @@
 
 import com.android.internal.net.LegacyVpnInfo;
 import com.android.internal.net.VpnConfig;
+import com.android.internal.net.VpnProfile;
 import com.android.internal.telephony.Phone;
 import com.android.internal.telephony.PhoneConstants;
 import com.android.server.am.BatteryStatsService;
 import com.android.server.connectivity.Tethering;
 import com.android.server.connectivity.Vpn;
+import com.android.server.net.BaseNetworkObserver;
+import com.android.server.net.LockdownVpnTracker;
 import com.google.android.collect.Lists;
 import com.google.android.collect.Sets;
+
 import dalvik.system.DexClassLoader;
+
 import java.io.FileDescriptor;
 import java.io.IOException;
 import java.io.PrintWriter;
 import java.lang.reflect.Constructor;
-import java.lang.reflect.Method;
-import java.lang.reflect.Modifier;
-import java.lang.reflect.InvocationTargetException;
 import java.net.Inet4Address;
 import java.net.Inet6Address;
 import java.net.InetAddress;
@@ -106,13 +124,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
@@ -126,7 +146,13 @@
     private Tethering mTethering;
     private boolean mTetheringConfigValid = false;
 
+    private KeyStore mKeyStore;
+
     private Vpn mVpn;
+    private VpnCallback mVpnCallback = new VpnCallback();
+
+    private boolean mLockdownEnabled;
+    private LockdownVpnTracker mLockdownTracker;
 
     /** Lock around {@link #mUidRules} and {@link #mMeteredIfaces}. */
     private Object mRulesLock = new Object();
@@ -142,6 +168,9 @@
      */
     private NetworkStateTracker mNetTrackers[];
 
+    /* Handles captive portal check on a network */
+    private CaptivePortalTracker mCaptivePortalTracker;
+
     /**
      * The link properties that define the current links
      */
@@ -186,95 +215,83 @@
     private static final boolean TO_DEFAULT_TABLE = true;
     private static final boolean TO_SECONDARY_TABLE = false;
 
-    // Share the event space with NetworkStateTracker (which can't see this
-    // internal class but sends us events).  If you change these, change
-    // NetworkStateTracker.java too.
-    private static final int MIN_NETWORK_STATE_TRACKER_EVENT = 1;
-    private static final int MAX_NETWORK_STATE_TRACKER_EVENT = 100;
-
     /**
      * used internally as a delayed event to make us switch back to the
      * default network
      */
-    private static final int EVENT_RESTORE_DEFAULT_NETWORK =
-            MAX_NETWORK_STATE_TRACKER_EVENT + 1;
+    private static final int EVENT_RESTORE_DEFAULT_NETWORK = 1;
 
     /**
      * used internally to change our mobile data enabled flag
      */
-    private static final int EVENT_CHANGE_MOBILE_DATA_ENABLED =
-            MAX_NETWORK_STATE_TRACKER_EVENT + 2;
+    private static final int EVENT_CHANGE_MOBILE_DATA_ENABLED = 2;
 
     /**
      * used internally to change our network preference setting
      * arg1 = networkType to prefer
      */
-    private static final int EVENT_SET_NETWORK_PREFERENCE =
-            MAX_NETWORK_STATE_TRACKER_EVENT + 3;
+    private static final int EVENT_SET_NETWORK_PREFERENCE = 3;
 
     /**
      * used internally to synchronize inet condition reports
      * arg1 = networkType
      * arg2 = condition (0 bad, 100 good)
      */
-    private static final int EVENT_INET_CONDITION_CHANGE =
-            MAX_NETWORK_STATE_TRACKER_EVENT + 4;
+    private static final int EVENT_INET_CONDITION_CHANGE = 4;
 
     /**
      * used internally to mark the end of inet condition hold periods
      * arg1 = networkType
      */
-    private static final int EVENT_INET_CONDITION_HOLD_END =
-            MAX_NETWORK_STATE_TRACKER_EVENT + 5;
+    private static final int EVENT_INET_CONDITION_HOLD_END = 5;
 
     /**
      * used internally to set enable/disable cellular data
      * arg1 = ENBALED or DISABLED
      */
-    private static final int EVENT_SET_MOBILE_DATA =
-            MAX_NETWORK_STATE_TRACKER_EVENT + 7;
+    private static final int EVENT_SET_MOBILE_DATA = 7;
 
     /**
      * used internally to clear a wakelock when transitioning
      * from one net to another
      */
-    private static final int EVENT_CLEAR_NET_TRANSITION_WAKELOCK =
-            MAX_NETWORK_STATE_TRACKER_EVENT + 8;
+    private static final int EVENT_CLEAR_NET_TRANSITION_WAKELOCK = 8;
 
     /**
      * used internally to reload global proxy settings
      */
-    private static final int EVENT_APPLY_GLOBAL_HTTP_PROXY =
-            MAX_NETWORK_STATE_TRACKER_EVENT + 9;
+    private static final int EVENT_APPLY_GLOBAL_HTTP_PROXY = 9;
 
     /**
      * used internally to set external dependency met/unmet
      * arg1 = ENABLED (met) or DISABLED (unmet)
      * arg2 = NetworkType
      */
-    private static final int EVENT_SET_DEPENDENCY_MET =
-            MAX_NETWORK_STATE_TRACKER_EVENT + 10;
+    private static final int EVENT_SET_DEPENDENCY_MET = 10;
 
     /**
      * used internally to restore DNS properties back to the
      * default network
      */
-    private static final int EVENT_RESTORE_DNS =
-            MAX_NETWORK_STATE_TRACKER_EVENT + 11;
+    private static final int EVENT_RESTORE_DNS = 11;
 
     /**
      * used internally to send a sticky broadcast delayed.
      */
-    private static final int EVENT_SEND_STICKY_BROADCAST_INTENT =
-            MAX_NETWORK_STATE_TRACKER_EVENT + 12;
+    private static final int EVENT_SEND_STICKY_BROADCAST_INTENT = 12;
 
     /**
      * Used internally to
      * {@link NetworkStateTracker#setPolicyDataEnable(boolean)}.
      */
-    private static final int EVENT_SET_POLICY_DATA_ENABLE = MAX_NETWORK_STATE_TRACKER_EVENT + 13;
+    private static final int EVENT_SET_POLICY_DATA_ENABLE = 13;
 
-    private Handler mHandler;
+    private static final int EVENT_VPN_STATE_CHANGED = 14;
+
+    /** Handler used for internal events. */
+    private InternalHandler mHandler;
+    /** Handler used for incoming {@link NetworkStateTracker} events. */
+    private NetworkStateTrackerHandler mTrackerHandler;
 
     // list of DeathRecipients used to make sure features are turned off when
     // a process dies
@@ -328,11 +345,24 @@
 
     public ConnectivityService(Context context, INetworkManagementService netd,
             INetworkStatsService statsService, INetworkPolicyManager policyManager) {
+        // Currently, omitting a NetworkFactory will create one internally
+        // TODO: create here when we have cleaner WiMAX support
+        this(context, netd, statsService, policyManager, null);
+    }
+
+    public ConnectivityService(Context context, INetworkManagementService netManager,
+            INetworkStatsService statsService, INetworkPolicyManager policyManager,
+            NetworkFactory netFactory) {
         if (DBG) log("ConnectivityService starting up");
 
         HandlerThread handlerThread = new HandlerThread("ConnectivityServiceThread");
         handlerThread.start();
-        mHandler = new MyHandler(handlerThread.getLooper());
+        mHandler = new InternalHandler(handlerThread.getLooper());
+        mTrackerHandler = new NetworkStateTrackerHandler(handlerThread.getLooper());
+
+        if (netFactory == null) {
+            netFactory = new DefaultNetworkFactory(context, mTrackerHandler);
+        }
 
         // setup our unique device name
         if (TextUtils.isEmpty(SystemProperties.get("net.hostname"))) {
@@ -358,8 +388,9 @@
         }
 
         mContext = checkNotNull(context, "missing Context");
-        mNetd = checkNotNull(netd, "missing INetworkManagementService");
+        mNetd = checkNotNull(netManager, "missing INetworkManagementService");
         mPolicyManager = checkNotNull(policyManager, "missing INetworkPolicyManager");
+        mKeyStore = KeyStore.getInstance();
 
         try {
             mPolicyManager.registerListener(mPolicyListener);
@@ -472,69 +503,38 @@
 
         mTestMode = SystemProperties.get("cm.test.mode").equals("true")
                 && SystemProperties.get("ro.build.type").equals("eng");
-        /*
-         * Create the network state trackers for Wi-Fi and mobile
-         * data. Maybe this could be done with a factory class,
-         * but it's not clear that it's worth it, given that
-         * the number of different network types is not going
-         * to change very often.
-         */
-        for (int netType : mPriorityList) {
-            switch (mNetConfigs[netType].radio) {
-            case ConnectivityManager.TYPE_WIFI:
-                mNetTrackers[netType] = new WifiStateTracker(netType,
-                        mNetConfigs[netType].name);
-                mNetTrackers[netType].startMonitoring(context, mHandler);
-               break;
-            case ConnectivityManager.TYPE_MOBILE:
-                mNetTrackers[netType] = new MobileDataStateTracker(netType,
-                        mNetConfigs[netType].name);
-                mNetTrackers[netType].startMonitoring(context, mHandler);
-                break;
-            case ConnectivityManager.TYPE_DUMMY:
-                mNetTrackers[netType] = new DummyDataStateTracker(netType,
-                        mNetConfigs[netType].name);
-                mNetTrackers[netType].startMonitoring(context, mHandler);
-                break;
-            case ConnectivityManager.TYPE_BLUETOOTH:
-                mNetTrackers[netType] = BluetoothTetheringDataTracker.getInstance();
-                mNetTrackers[netType].startMonitoring(context, mHandler);
-                break;
-            case ConnectivityManager.TYPE_WIMAX:
-                mNetTrackers[netType] = makeWimaxStateTracker();
-                if (mNetTrackers[netType]!= null) {
-                    mNetTrackers[netType].startMonitoring(context, mHandler);
-                }
-                break;
-            case ConnectivityManager.TYPE_ETHERNET:
-                mNetTrackers[netType] = EthernetDataTracker.getInstance();
-                mNetTrackers[netType].startMonitoring(context, mHandler);
-                break;
-            default:
-                loge("Trying to create a DataStateTracker for an unknown radio type " +
-                        mNetConfigs[netType].radio);
+
+        // Create and start trackers for hard-coded networks
+        for (int targetNetworkType : mPriorityList) {
+            final NetworkConfig config = mNetConfigs[targetNetworkType];
+            final NetworkStateTracker tracker;
+            try {
+                tracker = netFactory.createTracker(targetNetworkType, config);
+                mNetTrackers[targetNetworkType] = tracker;
+            } catch (IllegalArgumentException e) {
+                Slog.e(TAG, "Problem creating " + getNetworkTypeName(targetNetworkType)
+                        + " tracker: " + e);
                 continue;
             }
-            mCurrentLinkProperties[netType] = null;
-            if (mNetTrackers[netType] != null && mNetConfigs[netType].isDefault()) {
-                mNetTrackers[netType].reconnect();
+
+            tracker.startMonitoring(context, mTrackerHandler);
+            if (config.isDefault()) {
+                tracker.reconnect();
             }
         }
 
-        IBinder b = ServiceManager.getService(Context.NETWORKMANAGEMENT_SERVICE);
-        INetworkManagementService nmService = INetworkManagementService.Stub.asInterface(b);
-
-        mTethering = new Tethering(mContext, nmService, statsService, this, mHandler.getLooper());
+        mTethering = new Tethering(mContext, mNetd, statsService, this, mHandler.getLooper());
         mTetheringConfigValid = ((mTethering.getTetherableUsbRegexs().length != 0 ||
                                   mTethering.getTetherableWifiRegexs().length != 0 ||
                                   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 {
-            nmService.registerObserver(mTethering);
-            nmService.registerObserver(mVpn);
+            mNetd.registerObserver(mTethering);
+            mNetd.registerObserver(mDataActivityObserver);
         } catch (RemoteException e) {
             loge("Error registering observer :" + e);
         }
@@ -548,8 +548,55 @@
 
         loadGlobalProxy();
     }
-private NetworkStateTracker makeWimaxStateTracker() {
-        //Initialize Wimax
+
+    /**
+     * Factory that creates {@link NetworkStateTracker} instances using given
+     * {@link NetworkConfig}.
+     */
+    public interface NetworkFactory {
+        public NetworkStateTracker createTracker(int targetNetworkType, NetworkConfig config);
+    }
+
+    private static class DefaultNetworkFactory implements NetworkFactory {
+        private final Context mContext;
+        private final Handler mTrackerHandler;
+
+        public DefaultNetworkFactory(Context context, Handler trackerHandler) {
+            mContext = context;
+            mTrackerHandler = trackerHandler;
+        }
+
+        @Override
+        public NetworkStateTracker createTracker(int targetNetworkType, NetworkConfig config) {
+            switch (config.radio) {
+                case TYPE_WIFI:
+                    return new WifiStateTracker(targetNetworkType, config.name);
+                case TYPE_MOBILE:
+                    return new MobileDataStateTracker(targetNetworkType, config.name);
+                case TYPE_DUMMY:
+                    return new DummyDataStateTracker(targetNetworkType, config.name);
+                case TYPE_BLUETOOTH:
+                    return BluetoothTetheringDataTracker.getInstance();
+                case TYPE_WIMAX:
+                    return makeWimaxStateTracker(mContext, mTrackerHandler);
+                case TYPE_ETHERNET:
+                    return EthernetDataTracker.getInstance();
+                default:
+                    throw new IllegalArgumentException(
+                            "Trying to create a NetworkStateTracker for an unknown radio type: "
+                            + config.radio);
+            }
+        }
+    }
+
+    /**
+     * Loads external WiMAX library and registers as system service, returning a
+     * {@link NetworkStateTracker} for WiMAX. Caller is still responsible for
+     * invoking {@link NetworkStateTracker#startMonitoring(Context, Handler)}.
+     */
+    private static NetworkStateTracker makeWimaxStateTracker(
+            Context context, Handler trackerHandler) {
+        // Initialize Wimax
         DexClassLoader wimaxClassLoader;
         Class wimaxStateTrackerClass = null;
         Class wimaxServiceClass = null;
@@ -562,25 +609,25 @@
 
         NetworkStateTracker wimaxStateTracker = null;
 
-        boolean isWimaxEnabled = mContext.getResources().getBoolean(
+        boolean isWimaxEnabled = context.getResources().getBoolean(
                 com.android.internal.R.bool.config_wimaxEnabled);
 
         if (isWimaxEnabled) {
             try {
-                wimaxJarLocation = mContext.getResources().getString(
+                wimaxJarLocation = context.getResources().getString(
                         com.android.internal.R.string.config_wimaxServiceJarLocation);
-                wimaxLibLocation = mContext.getResources().getString(
+                wimaxLibLocation = context.getResources().getString(
                         com.android.internal.R.string.config_wimaxNativeLibLocation);
-                wimaxManagerClassName = mContext.getResources().getString(
+                wimaxManagerClassName = context.getResources().getString(
                         com.android.internal.R.string.config_wimaxManagerClassname);
-                wimaxServiceClassName = mContext.getResources().getString(
+                wimaxServiceClassName = context.getResources().getString(
                         com.android.internal.R.string.config_wimaxServiceClassname);
-                wimaxStateTrackerClassName = mContext.getResources().getString(
+                wimaxStateTrackerClassName = context.getResources().getString(
                         com.android.internal.R.string.config_wimaxStateTrackerClassname);
 
                 log("wimaxJarLocation: " + wimaxJarLocation);
                 wimaxClassLoader =  new DexClassLoader(wimaxJarLocation,
-                        new ContextWrapper(mContext).getCacheDir().getAbsolutePath(),
+                        new ContextWrapper(context).getCacheDir().getAbsolutePath(),
                         wimaxLibLocation, ClassLoader.getSystemClassLoader());
 
                 try {
@@ -601,13 +648,13 @@
 
                 Constructor wmxStTrkrConst = wimaxStateTrackerClass.getConstructor
                         (new Class[] {Context.class, Handler.class});
-                wimaxStateTracker = (NetworkStateTracker)wmxStTrkrConst.newInstance(mContext,
-                        mHandler);
+                wimaxStateTracker = (NetworkStateTracker) wmxStTrkrConst.newInstance(
+                        context, trackerHandler);
 
                 Constructor wmxSrvConst = wimaxServiceClass.getDeclaredConstructor
                         (new Class[] {Context.class, wimaxStateTrackerClass});
                 wmxSrvConst.setAccessible(true);
-                IBinder svcInvoker = (IBinder)wmxSrvConst.newInstance(mContext, wimaxStateTracker);
+                IBinder svcInvoker = (IBinder)wmxSrvConst.newInstance(context, wimaxStateTracker);
                 wmxSrvConst.setAccessible(false);
 
                 ServiceManager.addService(WimaxManagerConstants.WIMAX_SERVICE, svcInvoker);
@@ -623,6 +670,7 @@
 
         return wimaxStateTracker;
     }
+
     /**
      * Sets the preferred network.
      * @param preference the new preference
@@ -630,7 +678,8 @@
     public void setNetworkPreference(int preference) {
         enforceChangePermission();
 
-        mHandler.sendMessage(mHandler.obtainMessage(EVENT_SET_NETWORK_PREFERENCE, preference, 0));
+        mHandler.sendMessage(
+                mHandler.obtainMessage(EVENT_SET_NETWORK_PREFERENCE, preference, 0));
     }
 
     public int getNetworkPreference() {
@@ -749,6 +798,9 @@
             info = new NetworkInfo(info);
             info.setDetailedState(DetailedState.BLOCKED, null, null);
         }
+        if (mLockdownTracker != null) {
+            info = mLockdownTracker.augmentNetworkInfo(info);
+        }
         return info;
     }
 
@@ -766,6 +818,17 @@
         return getNetworkInfo(mActiveDefaultNetwork, uid);
     }
 
+    public NetworkInfo getActiveNetworkInfoUnfiltered() {
+        enforceAccessPermission();
+        if (isNetworkTypeValid(mActiveDefaultNetwork)) {
+            final NetworkStateTracker tracker = mNetTrackers[mActiveDefaultNetwork];
+            if (tracker != null) {
+                return tracker.getNetworkInfo();
+            }
+        }
+        return null;
+    }
+
     @Override
     public NetworkInfo getActiveNetworkInfoForUid(int uid) {
         enforceConnectivityInternalPermission();
@@ -923,6 +986,14 @@
         return tracker != null && tracker.setRadio(turnOn);
     }
 
+    private INetworkManagementEventObserver mDataActivityObserver = new BaseNetworkObserver() {
+        @Override
+        public void interfaceClassDataActivityChanged(String label, boolean active) {
+            int deviceType = Integer.parseInt(label);
+            sendDataActivityBroadcast(deviceType, active);
+        }
+    };
+
     /**
      * Used to notice when the calling process dies so we can self-expire
      *
@@ -1017,6 +1088,12 @@
             // TODO - move this into individual networktrackers
             int usedNetworkType = convertFeatureToNetworkType(networkType, feature);
 
+            if (mLockdownEnabled) {
+                // Since carrier APNs usually aren't available from VPN
+                // endpoint, mark them as unavailable.
+                return PhoneConstants.APN_TYPE_NOT_AVAILABLE;
+            }
+
             if (mProtectedNetworks.contains(usedNetworkType)) {
                 enforceConnectivityInternalPermission();
             }
@@ -1291,8 +1368,10 @@
             return false;
         }
         NetworkStateTracker tracker = mNetTrackers[networkType];
+        DetailedState netState = tracker.getNetworkInfo().getDetailedState();
 
-        if (tracker == null || !tracker.getNetworkInfo().isConnected() ||
+        if (tracker == null || (netState != DetailedState.CONNECTED &&
+                netState != DetailedState.CAPTIVE_PORTAL_CHECK) ||
                 tracker.isTeardownRequested()) {
             if (VDBG) {
                 log("requestRouteToHostAddress on down network " +
@@ -1591,6 +1670,10 @@
         int prevNetType = info.getType();
 
         mNetTrackers[prevNetType].setTeardownRequested(false);
+
+        // Remove idletimer previously setup in {@code handleConnect}
+        removeDataActivityTracking(prevNetType);
+
         /*
          * If the disconnected network is not the active one, then don't report
          * this as a loss of connectivity. What probably happened is that we're
@@ -1610,6 +1693,7 @@
 
         Intent intent = new Intent(ConnectivityManager.CONNECTIVITY_ACTION);
         intent.putExtra(ConnectivityManager.EXTRA_NETWORK_INFO, info);
+        intent.putExtra(ConnectivityManager.EXTRA_NETWORK_TYPE, info.getType());
         if (info.isFailover()) {
             intent.putExtra(ConnectivityManager.EXTRA_IS_FAILOVER, true);
             info.setFailover(false);
@@ -1719,7 +1803,7 @@
         }
     }
 
-    private void sendConnectedBroadcast(NetworkInfo info) {
+    public void sendConnectedBroadcast(NetworkInfo info) {
         sendGeneralBroadcast(info, CONNECTIVITY_ACTION_IMMEDIATE);
         sendGeneralBroadcast(info, CONNECTIVITY_ACTION);
     }
@@ -1734,8 +1818,13 @@
     }
 
     private Intent makeGeneralIntent(NetworkInfo info, String bcastType) {
+        if (mLockdownTracker != null) {
+            info = mLockdownTracker.augmentNetworkInfo(info);
+        }
+
         Intent intent = new Intent(bcastType);
         intent.putExtra(ConnectivityManager.EXTRA_NETWORK_INFO, info);
+        intent.putExtra(ConnectivityManager.EXTRA_NETWORK_TYPE, info.getType());
         if (info.isFailover()) {
             intent.putExtra(ConnectivityManager.EXTRA_IS_FAILOVER, true);
             info.setFailover(false);
@@ -1759,6 +1848,14 @@
         sendStickyBroadcastDelayed(makeGeneralIntent(info, bcastType), delayMs);
     }
 
+    private void sendDataActivityBroadcast(int deviceType, boolean active) {
+        Intent intent = new Intent(ConnectivityManager.ACTION_DATA_ACTIVITY_CHANGE);
+        intent.putExtra(ConnectivityManager.EXTRA_DEVICE_TYPE, deviceType);
+        intent.putExtra(ConnectivityManager.EXTRA_IS_ACTIVE, active);
+        mContext.sendOrderedBroadcastAsUser(intent, UserHandle.ALL,
+                RECEIVE_DATA_ACTIVITY_CHANGE, null, null, 0, null, null);
+    }
+
     /**
      * Called when an attempt to fail over to another network has failed.
      * @param info the {@link NetworkInfo} for the failed network
@@ -1779,6 +1876,7 @@
 
         Intent intent = new Intent(ConnectivityManager.CONNECTIVITY_ACTION);
         intent.putExtra(ConnectivityManager.EXTRA_NETWORK_INFO, info);
+        intent.putExtra(ConnectivityManager.EXTRA_NETWORK_TYPE, info.getType());
         if (getActiveNetworkInfo() == null) {
             intent.putExtra(ConnectivityManager.EXTRA_NO_CONNECTIVITY, true);
         }
@@ -1829,7 +1927,7 @@
                 log("sendStickyBroadcast: action=" + intent.getAction());
             }
 
-            mContext.sendStickyBroadcast(intent);
+            mContext.sendStickyBroadcastAsUser(intent, UserHandle.ALL);
         }
     }
 
@@ -1850,37 +1948,55 @@
         synchronized(this) {
             mSystemReady = true;
             if (mInitialBroadcast != null) {
-                mContext.sendStickyBroadcast(mInitialBroadcast);
+                mContext.sendStickyBroadcastAsUser(mInitialBroadcast, UserHandle.ALL);
                 mInitialBroadcast = null;
             }
         }
         // load the global proxy at startup
         mHandler.sendMessage(mHandler.obtainMessage(EVENT_APPLY_GLOBAL_HTTP_PROXY));
+
+        // Try bringing up tracker, but if KeyStore isn't ready yet, wait
+        // for user to unlock device.
+        if (!updateLockdownVpn()) {
+            final IntentFilter filter = new IntentFilter(Intent.ACTION_USER_PRESENT);
+            mContext.registerReceiver(mUserPresentReceiver, filter);
+        }
+    }
+
+    private BroadcastReceiver mUserPresentReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            // Try creating lockdown tracker, since user present usually means
+            // unlocked keystore.
+            if (updateLockdownVpn()) {
+                mContext.unregisterReceiver(this);
+            }
+        }
+    };
+
+    private boolean isNewNetTypePreferredOverCurrentNetType(int type) {
+        if ((type != mNetworkPreference &&
+                    mNetConfigs[mActiveDefaultNetwork].priority >
+                    mNetConfigs[type].priority) ||
+                mNetworkPreference == mActiveDefaultNetwork) return false;
+        return true;
     }
 
     private void handleConnect(NetworkInfo info) {
-        final int type = info.getType();
+        final int newNetType = info.getType();
+
+        setupDataActivityTracking(newNetType);
 
         // snapshot isFailover, because sendConnectedBroadcast() resets it
         boolean isFailover = info.isFailover();
-        final NetworkStateTracker thisNet = mNetTrackers[type];
+        final NetworkStateTracker thisNet = mNetTrackers[newNetType];
+        final String thisIface = thisNet.getLinkProperties().getInterfaceName();
 
         // if this is a default net and other default is running
         // kill the one not preferred
-        if (mNetConfigs[type].isDefault()) {
-            if (mActiveDefaultNetwork != -1 && mActiveDefaultNetwork != type) {
-                if ((type != mNetworkPreference &&
-                        mNetConfigs[mActiveDefaultNetwork].priority >
-                        mNetConfigs[type].priority) ||
-                        mNetworkPreference == mActiveDefaultNetwork) {
-                        // don't accept this one
-                        if (VDBG) {
-                            log("Not broadcasting CONNECT_ACTION " +
-                                "to torn down network " + info.getTypeName());
-                        }
-                        teardown(thisNet);
-                        return;
-                } else {
+        if (mNetConfigs[newNetType].isDefault()) {
+            if (mActiveDefaultNetwork != -1 && mActiveDefaultNetwork != newNetType) {
+                if (isNewNetTypePreferredOverCurrentNetType(newNetType)) {
                     // tear down the other
                     NetworkStateTracker otherNet =
                             mNetTrackers[mActiveDefaultNetwork];
@@ -1893,6 +2009,14 @@
                         teardown(thisNet);
                         return;
                     }
+                } else {
+                       // don't accept this one
+                        if (VDBG) {
+                            log("Not broadcasting CONNECT_ACTION " +
+                                "to torn down network " + info.getTypeName());
+                        }
+                        teardown(thisNet);
+                        return;
                 }
             }
             synchronized (ConnectivityService.this) {
@@ -1906,7 +2030,7 @@
                             1000);
                 }
             }
-            mActiveDefaultNetwork = type;
+            mActiveDefaultNetwork = newNetType;
             // this will cause us to come up initially as unconnected and switching
             // to connected after our normal pause unless somebody reports us as reall
             // disconnected
@@ -1918,20 +2042,99 @@
         }
         thisNet.setTeardownRequested(false);
         updateNetworkSettings(thisNet);
-        handleConnectivityChange(type, false);
+        handleConnectivityChange(newNetType, false);
         sendConnectedBroadcastDelayed(info, getConnectivityChangeDelay());
 
         // notify battery stats service about this network
-        final String iface = thisNet.getLinkProperties().getInterfaceName();
-        if (iface != null) {
+        if (thisIface != null) {
             try {
-                BatteryStatsService.getService().noteNetworkInterfaceType(iface, type);
+                BatteryStatsService.getService().noteNetworkInterfaceType(thisIface, newNetType);
             } catch (RemoteException e) {
                 // ignored; service lives in system_server
             }
         }
     }
 
+    private void handleCaptivePortalTrackerCheck(NetworkInfo info) {
+        if (DBG) log("Captive portal check " + info);
+        int type = info.getType();
+        final NetworkStateTracker thisNet = mNetTrackers[type];
+        if (mNetConfigs[type].isDefault()) {
+            if (mActiveDefaultNetwork != -1 && mActiveDefaultNetwork != type) {
+                if (isNewNetTypePreferredOverCurrentNetType(type)) {
+                    if (DBG) log("Captive check on " + info.getTypeName());
+                    mCaptivePortalTracker = CaptivePortalTracker.detect(mContext, info,
+                            ConnectivityService.this);
+                    return;
+                } else {
+                    if (DBG) log("Tear down low priority net " + info.getTypeName());
+                    teardown(thisNet);
+                    return;
+                }
+            }
+        }
+
+        thisNet.captivePortalCheckComplete();
+    }
+
+    /** @hide */
+    public void captivePortalCheckComplete(NetworkInfo info) {
+        mNetTrackers[info.getType()].captivePortalCheckComplete();
+        mCaptivePortalTracker = null;
+    }
+
+    /**
+     * Setup data activity tracking for the given network interface.
+     *
+     * Every {@code setupDataActivityTracking} should be paired with a
+     * {@link removeDataActivityTracking} for cleanup.
+     */
+    private void setupDataActivityTracking(int type) {
+        final NetworkStateTracker thisNet = mNetTrackers[type];
+        final String iface = thisNet.getLinkProperties().getInterfaceName();
+
+        final int timeout;
+
+        if (ConnectivityManager.isNetworkTypeMobile(type)) {
+            timeout = Settings.Secure.getInt(mContext.getContentResolver(),
+                                             Settings.Secure.DATA_ACTIVITY_TIMEOUT_MOBILE,
+                                             0);
+            // Canonicalize mobile network type
+            type = ConnectivityManager.TYPE_MOBILE;
+        } else if (ConnectivityManager.TYPE_WIFI == type) {
+            timeout = Settings.Secure.getInt(mContext.getContentResolver(),
+                                             Settings.Secure.DATA_ACTIVITY_TIMEOUT_WIFI,
+                                             0);
+        } else {
+            // do not track any other networks
+            timeout = 0;
+        }
+
+        if (timeout > 0 && iface != null) {
+            try {
+                mNetd.addIdleTimer(iface, timeout, Integer.toString(type));
+            } catch (RemoteException e) {
+            }
+        }
+    }
+
+    /**
+     * Remove data activity tracking when network disconnects.
+     */
+    private void removeDataActivityTracking(int type) {
+        final NetworkStateTracker net = mNetTrackers[type];
+        final String iface = net.getLinkProperties().getInterfaceName();
+
+        if (iface != null && (ConnectivityManager.isNetworkTypeMobile(type) ||
+                              ConnectivityManager.TYPE_WIFI == type)) {
+            try {
+                // the call fails silently if no idletimer setup for this interface
+                mNetd.removeIdleTimer(iface);
+            } catch (RemoteException e) {
+            }
+        }
+    }
+
     /**
      * After a change in the connectivity state of a network. We're mainly
      * concerned with making sure that the list of DNS servers is set up
@@ -2136,9 +2339,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
@@ -2264,7 +2467,7 @@
          * Connectivity events can happen before boot has completed ...
          */
         intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
-        mContext.sendBroadcast(intent);
+        mContext.sendBroadcastAsUser(intent, UserHandle.ALL);
     }
 
     // Caller must grab mDnsLock.
@@ -2431,8 +2634,8 @@
     }
 
     // must be stateless - things change under us.
-    private class MyHandler extends Handler {
-        public MyHandler(Looper looper) {
+    private class NetworkStateTrackerHandler extends Handler {
+        public NetworkStateTrackerHandler(Looper looper) {
             super(looper);
         }
 
@@ -2468,6 +2671,9 @@
                     if (info.getDetailedState() ==
                             NetworkInfo.DetailedState.FAILED) {
                         handleConnectionFailure(info);
+                    } else if (info.getDetailedState() ==
+                            DetailedState.CAPTIVE_PORTAL_CHECK) {
+                        handleCaptivePortalTrackerCheck(info);
                     } else if (state == NetworkInfo.State.DISCONNECTED) {
                         handleDisconnect(info);
                     } else if (state == NetworkInfo.State.SUSPENDED) {
@@ -2482,6 +2688,9 @@
                     } else if (state == NetworkInfo.State.CONNECTED) {
                         handleConnect(info);
                     }
+                    if (mLockdownTracker != null) {
+                        mLockdownTracker.onNetworkInfoChanged(info);
+                    }
                     break;
                 case NetworkStateTracker.EVENT_CONFIGURATION_CHANGED:
                     info = (NetworkInfo) msg.obj;
@@ -2495,6 +2704,19 @@
                     type = info.getType();
                     updateNetworkSettings(mNetTrackers[type]);
                     break;
+            }
+        }
+    }
+
+    private class InternalHandler extends Handler {
+        public InternalHandler(Looper looper) {
+            super(looper);
+        }
+
+        @Override
+        public void handleMessage(Message msg) {
+            NetworkInfo info;
+            switch (msg.what) {
                 case EVENT_CLEAR_NET_TRANSITION_WAKELOCK:
                     String causedBy = null;
                     synchronized (ConnectivityService.this) {
@@ -2566,6 +2788,13 @@
                     final int networkType = msg.arg1;
                     final boolean enabled = msg.arg2 == ENABLED;
                     handleSetPolicyDataEnable(networkType, enabled);
+                    break;
+                }
+                case EVENT_VPN_STATE_CHANGED: {
+                    if (mLockdownTracker != null) {
+                        mLockdownTracker.onVpnStateChanged((NetworkInfo) msg.obj);
+                    }
+                    break;
                 }
             }
         }
@@ -2883,7 +3112,7 @@
         intent.addFlags(Intent.FLAG_RECEIVER_REPLACE_PENDING |
             Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
         intent.putExtra(Proxy.EXTRA_PROXY_INFO, proxy);
-        mContext.sendStickyBroadcast(intent);
+        mContext.sendStickyBroadcastAsUser(intent, UserHandle.ALL);
     }
 
     private static class SettingsObserver extends ContentObserver {
@@ -2907,11 +3136,11 @@
         }
     }
 
-    private void log(String s) {
+    private static void log(String s) {
         Slog.d(TAG, s);
     }
 
-    private void loge(String s) {
+    private static void loge(String s) {
         Slog.e(TAG, s);
     }
 
@@ -2964,6 +3193,7 @@
      */
     @Override
     public boolean protectVpn(ParcelFileDescriptor socket) {
+        throwIfLockdownEnabled();
         try {
             int type = mActiveDefaultNetwork;
             if (ConnectivityManager.isNetworkTypeValid(type)) {
@@ -2990,6 +3220,7 @@
      */
     @Override
     public boolean prepareVpn(String oldPackage, String newPackage) {
+        throwIfLockdownEnabled();
         return mVpn.prepare(oldPackage, newPackage);
     }
 
@@ -3002,18 +3233,22 @@
      */
     @Override
     public ParcelFileDescriptor establishVpn(VpnConfig config) {
+        throwIfLockdownEnabled();
         return mVpn.establish(config);
     }
 
     /**
-     * Start legacy VPN and return an intent to VpnDialogs. This method is
-     * used by VpnSettings and not available in ConnectivityManager.
-     * Permissions are checked in Vpn class.
-     * @hide
+     * Start legacy VPN, controlling native daemons as needed. Creates a
+     * secondary thread to perform connection work, returning quickly.
      */
     @Override
-    public void startLegacyVpn(VpnConfig config, String[] racoon, String[] mtpd) {
-        mVpn.startLegacyVpn(config, racoon, mtpd);
+    public void startLegacyVpn(VpnProfile profile) {
+        throwIfLockdownEnabled();
+        final LinkProperties egress = getActiveLinkProperties();
+        if (egress == null) {
+            throw new IllegalStateException("Missing active network connection");
+        }
+        mVpn.startLegacyVpn(profile, mKeyStore, egress);
     }
 
     /**
@@ -3024,6 +3259,7 @@
      */
     @Override
     public LegacyVpnInfo getLegacyVpnInfo() {
+        throwIfLockdownEnabled();
         return mVpn.getLegacyVpnInfo();
     }
 
@@ -3038,10 +3274,13 @@
      * be done whenever a better abstraction is developed.
      */
     public class VpnCallback {
-
         private VpnCallback() {
         }
 
+        public void onStateChanged(NetworkInfo info) {
+            mHandler.obtainMessage(EVENT_VPN_STATE_CHANGED, info).sendToTarget();
+        }
+
         public void override(List<String> dnsServers, List<String> searchDomains) {
             if (dnsServers == null) {
                 restore();
@@ -3108,4 +3347,58 @@
             }
         }
     }
+
+    @Override
+    public boolean updateLockdownVpn() {
+        mContext.enforceCallingOrSelfPermission(CONNECTIVITY_INTERNAL, TAG);
+
+        // Tear down existing lockdown if profile was removed
+        mLockdownEnabled = LockdownVpnTracker.isEnabled();
+        if (mLockdownEnabled) {
+            if (mKeyStore.state() != KeyStore.State.UNLOCKED) {
+                Slog.w(TAG, "KeyStore locked; unable to create LockdownTracker");
+                return false;
+            }
+
+            final String profileName = new String(mKeyStore.get(Credentials.LOCKDOWN_VPN));
+            final VpnProfile profile = VpnProfile.decode(
+                    profileName, mKeyStore.get(Credentials.VPN + profileName));
+            setLockdownTracker(new LockdownVpnTracker(mContext, mNetd, this, mVpn, profile));
+        } else {
+            setLockdownTracker(null);
+        }
+
+        return true;
+    }
+
+    /**
+     * Internally set new {@link LockdownVpnTracker}, shutting down any existing
+     * {@link LockdownVpnTracker}. Can be {@code null} to disable lockdown.
+     */
+    private void setLockdownTracker(LockdownVpnTracker tracker) {
+        // Shutdown any existing tracker
+        final LockdownVpnTracker existing = mLockdownTracker;
+        mLockdownTracker = null;
+        if (existing != null) {
+            existing.shutdown();
+        }
+
+        try {
+            if (tracker != null) {
+                mNetd.setFirewallEnabled(true);
+                mLockdownTracker = tracker;
+                mLockdownTracker.init();
+            } else {
+                mNetd.setFirewallEnabled(false);
+            }
+        } catch (RemoteException e) {
+            // ignored; NMS lives inside system_server
+        }
+    }
+
+    private void throwIfLockdownEnabled() {
+        if (mLockdownEnabled) {
+            throw new IllegalStateException("Unavailable in lockdown mode");
+        }
+    }
 }
diff --git a/services/tests/servicestests/src/com/android/server/ConnectivityServiceTest.java b/services/tests/servicestests/src/com/android/server/ConnectivityServiceTest.java
new file mode 100644
index 0000000..93ea6a2
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/ConnectivityServiceTest.java
@@ -0,0 +1,224 @@
+/*
+ * Copyright (C) 2012 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;
+
+import static android.net.ConnectivityManager.CONNECTIVITY_ACTION_IMMEDIATE;
+import static android.net.ConnectivityManager.TYPE_MOBILE;
+import static android.net.ConnectivityManager.TYPE_WIFI;
+import static android.net.ConnectivityManager.getNetworkTypeName;
+import static android.net.NetworkStateTracker.EVENT_STATE_CHANGED;
+import static com.google.testing.littlemock.LittleMock.anyInt;
+import static com.google.testing.littlemock.LittleMock.createCaptor;
+import static com.google.testing.littlemock.LittleMock.doNothing;
+import static com.google.testing.littlemock.LittleMock.doReturn;
+import static com.google.testing.littlemock.LittleMock.doThrow;
+import static com.google.testing.littlemock.LittleMock.eq;
+import static com.google.testing.littlemock.LittleMock.isA;
+import static com.google.testing.littlemock.LittleMock.mock;
+import static com.google.testing.littlemock.LittleMock.reset;
+import static com.google.testing.littlemock.LittleMock.verify;
+
+import android.content.Context;
+import android.net.INetworkPolicyManager;
+import android.net.INetworkStatsService;
+import android.net.LinkProperties;
+import android.net.NetworkConfig;
+import android.net.NetworkInfo;
+import android.net.NetworkInfo.DetailedState;
+import android.net.NetworkStateTracker;
+import android.net.RouteInfo;
+import android.os.Handler;
+import android.os.INetworkManagementService;
+import android.test.AndroidTestCase;
+import android.test.suitebuilder.annotation.LargeTest;
+import android.util.Log;
+import android.util.LogPrinter;
+
+import com.google.testing.littlemock.ArgumentCaptor;
+
+import java.net.InetAddress;
+import java.util.concurrent.Future;
+
+/**
+ * Tests for {@link ConnectivityService}.
+ */
+@LargeTest
+public class ConnectivityServiceTest extends AndroidTestCase {
+    private static final String TAG = "ConnectivityServiceTest";
+
+    private static final String MOBILE_IFACE = "rmnet3";
+    private static final String WIFI_IFACE = "wlan6";
+
+    private static final RouteInfo MOBILE_ROUTE_V4 = RouteInfo.makeHostRoute(parse("10.0.0.33"));
+    private static final RouteInfo MOBILE_ROUTE_V6 = RouteInfo.makeHostRoute(parse("fd00::33"));
+
+    private static final RouteInfo WIFI_ROUTE_V4 = RouteInfo.makeHostRoute(
+            parse("192.168.0.66"), parse("192.168.0.1"));
+    private static final RouteInfo WIFI_ROUTE_V6 = RouteInfo.makeHostRoute(
+            parse("fd00::66"), parse("fd00::"));
+
+    private INetworkManagementService mNetManager;
+    private INetworkStatsService mStatsService;
+    private INetworkPolicyManager mPolicyService;
+    private ConnectivityService.NetworkFactory mNetFactory;
+
+    private BroadcastInterceptingContext mServiceContext;
+    private ConnectivityService mService;
+
+    private MockNetwork mMobile;
+    private MockNetwork mWifi;
+
+    private Handler mTrackerHandler;
+
+    private static class MockNetwork {
+        public NetworkStateTracker tracker;
+        public NetworkInfo info;
+        public LinkProperties link;
+
+        public MockNetwork(int type) {
+            tracker = mock(NetworkStateTracker.class);
+            info = new NetworkInfo(type, -1, getNetworkTypeName(type), null);
+            link = new LinkProperties();
+        }
+
+        public void doReturnDefaults() {
+            // TODO: eventually CS should make defensive copies
+            doReturn(new NetworkInfo(info)).when(tracker).getNetworkInfo();
+            doReturn(new LinkProperties(link)).when(tracker).getLinkProperties();
+
+            // fallback to default TCP buffers
+            doReturn("").when(tracker).getTcpBufferSizesPropName();
+        }
+    }
+
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+
+        mServiceContext = new BroadcastInterceptingContext(getContext());
+
+        mNetManager = mock(INetworkManagementService.class);
+        mStatsService = mock(INetworkStatsService.class);
+        mPolicyService = mock(INetworkPolicyManager.class);
+        mNetFactory = mock(ConnectivityService.NetworkFactory.class);
+
+        mMobile = new MockNetwork(TYPE_MOBILE);
+        mWifi = new MockNetwork(TYPE_WIFI);
+
+        // omit most network trackers
+        doThrow(new IllegalArgumentException("Not supported in test environment"))
+                .when(mNetFactory).createTracker(anyInt(), isA(NetworkConfig.class));
+
+        doReturn(mMobile.tracker)
+                .when(mNetFactory).createTracker(eq(TYPE_MOBILE), isA(NetworkConfig.class));
+        doReturn(mWifi.tracker)
+                .when(mNetFactory).createTracker(eq(TYPE_WIFI), isA(NetworkConfig.class));
+
+        final ArgumentCaptor<Handler> trackerHandler = createCaptor();
+        doNothing().when(mMobile.tracker)
+                .startMonitoring(isA(Context.class), trackerHandler.capture());
+
+        mService = new ConnectivityService(
+                mServiceContext, mNetManager, mStatsService, mPolicyService, mNetFactory);
+        mService.systemReady();
+
+        mTrackerHandler = trackerHandler.getValue();
+        mTrackerHandler.getLooper().setMessageLogging(new LogPrinter(Log.INFO, TAG));
+    }
+
+    @Override
+    public void tearDown() throws Exception {
+        super.tearDown();
+    }
+
+    public void testMobileConnectedAddedRoutes() throws Exception {
+        Future<?> nextConnBroadcast;
+
+        // bring up mobile network
+        mMobile.info.setDetailedState(DetailedState.CONNECTED, null, null);
+        mMobile.link.setInterfaceName(MOBILE_IFACE);
+        mMobile.link.addRoute(MOBILE_ROUTE_V4);
+        mMobile.link.addRoute(MOBILE_ROUTE_V6);
+        mMobile.doReturnDefaults();
+
+        nextConnBroadcast = mServiceContext.nextBroadcastIntent(CONNECTIVITY_ACTION_IMMEDIATE);
+        mTrackerHandler.obtainMessage(EVENT_STATE_CHANGED, mMobile.info).sendToTarget();
+        nextConnBroadcast.get();
+
+        // verify that both routes were added and DNS was flushed
+        verify(mNetManager).addRoute(eq(MOBILE_IFACE), eq(MOBILE_ROUTE_V4));
+        verify(mNetManager).addRoute(eq(MOBILE_IFACE), eq(MOBILE_ROUTE_V6));
+        verify(mNetManager).flushInterfaceDnsCache(MOBILE_IFACE);
+
+    }
+
+    public void testMobileWifiHandoff() throws Exception {
+        Future<?> nextConnBroadcast;
+
+        // bring up mobile network
+        mMobile.info.setDetailedState(DetailedState.CONNECTED, null, null);
+        mMobile.link.setInterfaceName(MOBILE_IFACE);
+        mMobile.link.addRoute(MOBILE_ROUTE_V4);
+        mMobile.link.addRoute(MOBILE_ROUTE_V6);
+        mMobile.doReturnDefaults();
+
+        nextConnBroadcast = mServiceContext.nextBroadcastIntent(CONNECTIVITY_ACTION_IMMEDIATE);
+        mTrackerHandler.obtainMessage(EVENT_STATE_CHANGED, mMobile.info).sendToTarget();
+        nextConnBroadcast.get();
+
+        reset(mNetManager);
+
+        // now bring up wifi network
+        mWifi.info.setDetailedState(DetailedState.CONNECTED, null, null);
+        mWifi.link.setInterfaceName(WIFI_IFACE);
+        mWifi.link.addRoute(WIFI_ROUTE_V4);
+        mWifi.link.addRoute(WIFI_ROUTE_V6);
+        mWifi.doReturnDefaults();
+
+        // expect that mobile will be torn down
+        doReturn(true).when(mMobile.tracker).teardown();
+
+        nextConnBroadcast = mServiceContext.nextBroadcastIntent(CONNECTIVITY_ACTION_IMMEDIATE);
+        mTrackerHandler.obtainMessage(EVENT_STATE_CHANGED, mWifi.info).sendToTarget();
+        nextConnBroadcast.get();
+
+        // verify that wifi routes added, and teardown requested
+        verify(mNetManager).addRoute(eq(WIFI_IFACE), eq(WIFI_ROUTE_V4));
+        verify(mNetManager).addRoute(eq(WIFI_IFACE), eq(WIFI_ROUTE_V6));
+        verify(mNetManager).flushInterfaceDnsCache(WIFI_IFACE);
+        verify(mMobile.tracker).teardown();
+
+        reset(mNetManager, mMobile.tracker);
+
+        // tear down mobile network, as requested
+        mMobile.info.setDetailedState(DetailedState.DISCONNECTED, null, null);
+        mMobile.link.clear();
+        mMobile.doReturnDefaults();
+
+        nextConnBroadcast = mServiceContext.nextBroadcastIntent(CONNECTIVITY_ACTION_IMMEDIATE);
+        mTrackerHandler.obtainMessage(EVENT_STATE_CHANGED, mMobile.info).sendToTarget();
+        nextConnBroadcast.get();
+
+        verify(mNetManager).removeRoute(eq(MOBILE_IFACE), eq(MOBILE_ROUTE_V4));
+        verify(mNetManager).removeRoute(eq(MOBILE_IFACE), eq(MOBILE_ROUTE_V6));
+
+    }
+
+    private static InetAddress parse(String addr) {
+        return InetAddress.parseNumericAddress(addr);
+    }
+}