Improve Wi-Fi hand-off

When Wi-fi connects at L2 layer, the beacons reach and the device
can maintain a connection to the access point, but the application
connectivity can be flaky (due to bigger packet size exchange).

We now use Watchdog to monitor the quality of the last hop on
Wi-Fi using signal strength and ARP connectivity as indicators
to decide if the link is good enough to switch to Wi-Fi as the uplink.

ARP pings are useful for link validation but can still get through
when the application traffic fails to go through and thus not best indicator
real packet loss since they are tiny packets (28 bytes) and have
much low chance of packet corruption than the regular data
packets.

Signal strength and ARP used together ends up working well in tests.
The goal is to switch to Wi-Fi after validating ARP transfer
and RSSI and then switching out of Wi-Fi when we hit a low
signal strength threshold and waiting until the signal strength
improves and validating ARP transfer.

Change-Id: Ica593291ec7772da892f03cf45b649635b730c47
diff --git a/api/current.txt b/api/current.txt
index 66c8eb2..6290d30 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -11807,6 +11807,7 @@
     enum_constant public static final android.net.NetworkInfo.DetailedState OBTAINING_IPADDR;
     enum_constant public static final android.net.NetworkInfo.DetailedState SCANNING;
     enum_constant public static final android.net.NetworkInfo.DetailedState SUSPENDED;
+    enum_constant public static final android.net.NetworkInfo.DetailedState VERIFYING_POOR_LINK;
   }
 
   public static final class NetworkInfo.State extends java.lang.Enum {
diff --git a/core/java/android/net/NetworkInfo.java b/core/java/android/net/NetworkInfo.java
index 2f43cb8..0bc6b58 100644
--- a/core/java/android/net/NetworkInfo.java
+++ b/core/java/android/net/NetworkInfo.java
@@ -77,7 +77,9 @@
         /** Attempt to connect failed. */
         FAILED,
         /** Access to this network is blocked. */
-        BLOCKED
+        BLOCKED,
+        /** Link has poor connectivity. */
+        VERIFYING_POOR_LINK
     }
 
     /**
@@ -94,6 +96,7 @@
         stateMap.put(DetailedState.CONNECTING, State.CONNECTING);
         stateMap.put(DetailedState.AUTHENTICATING, State.CONNECTING);
         stateMap.put(DetailedState.OBTAINING_IPADDR, State.CONNECTING);
+        stateMap.put(DetailedState.VERIFYING_POOR_LINK, State.CONNECTING);
         stateMap.put(DetailedState.CONNECTED, State.CONNECTED);
         stateMap.put(DetailedState.SUSPENDED, State.SUSPENDED);
         stateMap.put(DetailedState.DISCONNECTING, State.DISCONNECTING);
diff --git a/core/java/android/net/arp/ArpPeer.java b/core/java/android/net/arp/ArpPeer.java
new file mode 100644
index 0000000..8e666bc
--- /dev/null
+++ b/core/java/android/net/arp/ArpPeer.java
@@ -0,0 +1,132 @@
+/*
+ * 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 android.net.arp;
+
+import android.os.SystemClock;
+import android.util.Log;
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.Inet6Address;
+import java.net.SocketException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.Arrays;
+
+import libcore.net.RawSocket;
+
+/**
+ * This class allows simple ARP exchanges over an uninitialized network
+ * interface.
+ *
+ * @hide
+ */
+public class ArpPeer {
+    private String mInterfaceName;
+    private final InetAddress mMyAddr;
+    private final byte[] mMyMac = new byte[6];
+    private final InetAddress mPeer;
+    private final RawSocket mSocket;
+    private final byte[] L2_BROADCAST;  // TODO: refactor from DhcpClient.java
+    private static final int MAX_LENGTH = 1500; // refactor from DhcpPacket.java
+    private static final int ETHERNET_TYPE = 1;
+    private static final int ARP_LENGTH = 28;
+    private static final int MAC_ADDR_LENGTH = 6;
+    private static final int IPV4_LENGTH = 4;
+    private static final String TAG = "ArpPeer";
+
+    public ArpPeer(String interfaceName, InetAddress myAddr, String mac,
+                   InetAddress peer) throws SocketException {
+        mInterfaceName = interfaceName;
+        mMyAddr = myAddr;
+
+        for (int i = 0; i < MAC_ADDR_LENGTH; i++) {
+            mMyMac[i] = (byte) Integer.parseInt(mac.substring(
+                        i*3, (i*3) + 2), 16);
+        }
+
+        if (myAddr instanceof Inet6Address || peer instanceof Inet6Address) {
+            throw new IllegalArgumentException("IPv6 unsupported");
+        }
+
+        mPeer = peer;
+        L2_BROADCAST = new byte[MAC_ADDR_LENGTH];
+        Arrays.fill(L2_BROADCAST, (byte) 0xFF);
+
+        mSocket = new RawSocket(mInterfaceName, RawSocket.ETH_P_ARP);
+    }
+
+    /**
+     * Returns the MAC address (or null if timeout) for the requested
+     * peer.
+     */
+    public byte[] doArp(int timeoutMillis) {
+        ByteBuffer buf = ByteBuffer.allocate(MAX_LENGTH);
+        byte[] desiredIp = mPeer.getAddress();
+        long timeout = SystemClock.elapsedRealtime() + timeoutMillis;
+
+        // construct ARP request packet, using a ByteBuffer as a
+        // convenient container
+        buf.clear();
+        buf.order(ByteOrder.BIG_ENDIAN);
+
+        buf.putShort((short) ETHERNET_TYPE); // Ethernet type, 16 bits
+        buf.putShort(RawSocket.ETH_P_IP); // Protocol type IP, 16 bits
+        buf.put((byte)MAC_ADDR_LENGTH);  // MAC address length, 6 bytes
+        buf.put((byte)IPV4_LENGTH);  // IPv4 protocol size
+        buf.putShort((short) 1); // ARP opcode 1: 'request'
+        buf.put(mMyMac);        // six bytes: sender MAC
+        buf.put(mMyAddr.getAddress());  // four bytes: sender IP address
+        buf.put(new byte[MAC_ADDR_LENGTH]); // target MAC address: unknown
+        buf.put(desiredIp); // target IP address, 4 bytes
+        buf.flip();
+        mSocket.write(L2_BROADCAST, buf.array(), 0, buf.limit());
+
+        byte[] recvBuf = new byte[MAX_LENGTH];
+
+        while (SystemClock.elapsedRealtime() < timeout) {
+            long duration = (long) timeout - SystemClock.elapsedRealtime();
+            int readLen = mSocket.read(recvBuf, 0, recvBuf.length, -1,
+                (int) duration);
+
+            // Verify packet details. see RFC 826
+            if ((readLen >= ARP_LENGTH) // trailing bytes at times
+                && (recvBuf[0] == 0) && (recvBuf[1] == ETHERNET_TYPE) // type Ethernet
+                && (recvBuf[2] == 8) && (recvBuf[3] == 0) // protocol IP
+                && (recvBuf[4] == MAC_ADDR_LENGTH) // mac length
+                && (recvBuf[5] == IPV4_LENGTH) // IPv4 protocol size
+                && (recvBuf[6] == 0) && (recvBuf[7] == 2) // ARP reply
+                // verify desired IP address
+                && (recvBuf[14] == desiredIp[0]) && (recvBuf[15] == desiredIp[1])
+                && (recvBuf[16] == desiredIp[2]) && (recvBuf[17] == desiredIp[3]))
+            {
+                // looks good.  copy out the MAC
+                byte[] result = new byte[MAC_ADDR_LENGTH];
+                System.arraycopy(recvBuf, 8, result, 0, MAC_ADDR_LENGTH);
+                return result;
+            }
+        }
+
+        return null;
+    }
+
+    public void close() {
+        try {
+            mSocket.close();
+        } catch (IOException ex) {
+        }
+    }
+}
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index 0aad64a..b42417a 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -3125,15 +3125,8 @@
          * ms delay before rechecking an 'online' wifi connection when it is thought to be unstable.
          * @hide
          */
-        public static final String WIFI_WATCHDOG_DNS_CHECK_SHORT_INTERVAL_MS =
-                "wifi_watchdog_dns_check_short_interval_ms";
-
-        /**
-         * ms delay before rechecking an 'online' wifi connection when it is thought to be stable.
-         * @hide
-         */
-        public static final String WIFI_WATCHDOG_DNS_CHECK_LONG_INTERVAL_MS =
-                "wifi_watchdog_dns_check_long_interval_ms";
+        public static final String WIFI_WATCHDOG_ARP_CHECK_INTERVAL_MS =
+                "wifi_watchdog_arp_interval_ms";
 
         /**
          * ms delay before rechecking a connect SSID for walled garden with a http download.
@@ -3143,44 +3136,28 @@
                 "wifi_watchdog_walled_garden_interval_ms";
 
         /**
-         * max blacklist calls on an SSID before full dns check failures disable the network.
+         * Number of ARP pings per check.
          * @hide
          */
-        public static final String WIFI_WATCHDOG_MAX_SSID_BLACKLISTS =
-                "wifi_watchdog_max_ssid_blacklists";
+        public static final String WIFI_WATCHDOG_NUM_ARP_PINGS = "wifi_watchdog_num_arp_pings";
 
         /**
-         * Number of dns pings per check.
+         * Minimum number of responses to the arp pings to consider the test 'successful'.
          * @hide
          */
-        public static final String WIFI_WATCHDOG_NUM_DNS_PINGS = "wifi_watchdog_num_dns_pings";
+        public static final String WIFI_WATCHDOG_MIN_ARP_RESPONSES =
+                "wifi_watchdog_min_arp_responses";
 
         /**
-         * Minimum number of responses to the dns pings to consider the test 'successful'.
+         * Timeout on ARP pings
          * @hide
          */
-        public static final String WIFI_WATCHDOG_MIN_DNS_RESPONSES =
-                "wifi_watchdog_min_dns_responses";
+        public static final String WIFI_WATCHDOG_ARP_PING_TIMEOUT_MS =
+                "wifi_watchdog_arp_ping_timeout_ms";
 
         /**
-         * Timeout on dns pings
-         * @hide
-         */
-        public static final String WIFI_WATCHDOG_DNS_PING_TIMEOUT_MS =
-                "wifi_watchdog_dns_ping_timeout_ms";
-
-        /**
-         * We consider action from a 'blacklist' call to have finished by the end of
-         * this interval.  If we are connected to the same AP with no network connection,
-         * we are likely stuck on an SSID with no external connectivity.
-         * @hide
-         */
-        public static final String WIFI_WATCHDOG_BLACKLIST_FOLLOWUP_INTERVAL_MS =
-                "wifi_watchdog_blacklist_followup_interval_ms";
-
-        /**
-         * Setting to turn off poor network avoidance on Wi-Fi. Feature is disabled by default and
-         * the setting needs to be set to 1 to enable it.
+         * Setting to turn off poor network avoidance on Wi-Fi. Feature is enabled by default and
+         * the setting needs to be set to 0 to disable it.
          * @hide
          */
         public static final String WIFI_WATCHDOG_POOR_NETWORK_TEST_ENABLED =
@@ -3204,14 +3181,6 @@
                 "wifi_watchdog_walled_garden_url";
 
         /**
-         * Boolean to determine whether to notify on disabling a network.  Secure setting used
-         * to notify user only once.
-         * @hide
-         */
-        public static final String WIFI_WATCHDOG_SHOW_DISABLED_NETWORK_POPUP =
-                "wifi_watchdog_show_disabled_network_popup";
-
-        /**
          * The maximum number of times we will retry a connection to an access
          * point for which we have failed in acquiring an IP address from DHCP.
          * A value of N means that we will make N+1 connection attempts in all.
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/NetworkController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/NetworkController.java
index c59290c..95704f6 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/NetworkController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/NetworkController.java
@@ -203,7 +203,7 @@
         mWifiManager = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
         Handler handler = new WifiHandler();
         mWifiChannel = new AsyncChannel();
-        Messenger wifiMessenger = mWifiManager.getMessenger();
+        Messenger wifiMessenger = mWifiManager.getWifiServiceMessenger();
         if (wifiMessenger != null) {
             mWifiChannel.connect(mContext, handler, wifiMessenger);
         }
@@ -767,17 +767,10 @@
             } else if (!mWifiConnected) {
                 mWifiSsid = null;
             }
-            // Apparently the wifi level is not stable at this point even if we've just connected to
-            // the network; we need to wait for an RSSI_CHANGED_ACTION for that. So let's just set
-            // it to 0 for now
-            mWifiLevel = 0;
-            mWifiRssi = -200;
         } else if (action.equals(WifiManager.RSSI_CHANGED_ACTION)) {
-            if (mWifiConnected) {
-                mWifiRssi = intent.getIntExtra(WifiManager.EXTRA_NEW_RSSI, -200);
-                mWifiLevel = WifiManager.calculateSignalLevel(
-                        mWifiRssi, WifiIcons.WIFI_LEVEL_COUNT);
-            }
+            mWifiRssi = intent.getIntExtra(WifiManager.EXTRA_NEW_RSSI, -200);
+            mWifiLevel = WifiManager.calculateSignalLevel(
+                    mWifiRssi, WifiIcons.WIFI_LEVEL_COUNT);
         }
 
         updateWifiIcons();
diff --git a/services/java/com/android/server/WifiService.java b/services/java/com/android/server/WifiService.java
index 5208785..f09c43f 100644
--- a/services/java/com/android/server/WifiService.java
+++ b/services/java/com/android/server/WifiService.java
@@ -914,7 +914,7 @@
      * Get a reference to handler. This is used by a client to establish
      * an AsyncChannel communication with WifiService
      */
-    public Messenger getMessenger() {
+    public Messenger getWifiServiceMessenger() {
         /* Enforce the highest permissions
            TODO: when we consider exposing the asynchronous API, think about
                  how to provide both access and change permissions seperately
@@ -924,6 +924,13 @@
         return new Messenger(mAsyncServiceHandler);
     }
 
+    /** Get a reference to WifiStateMachine handler for AsyncChannel communication */
+    public Messenger getWifiStateMachineMessenger() {
+        enforceAccessPermission();
+        enforceChangePermission();
+        return mWifiStateMachine.getMessenger();
+    }
+
     /**
      * Get the IP and proxy configuration file
      */
diff --git a/wifi/java/android/net/wifi/IWifiManager.aidl b/wifi/java/android/net/wifi/IWifiManager.aidl
index 61dfebf..6b08074 100644
--- a/wifi/java/android/net/wifi/IWifiManager.aidl
+++ b/wifi/java/android/net/wifi/IWifiManager.aidl
@@ -101,7 +101,9 @@
 
     void clearBlacklist();
 
-    Messenger getMessenger();
+    Messenger getWifiServiceMessenger();
+
+    Messenger getWifiStateMachineMessenger();
 
     String getConfigFile();
 }
diff --git a/wifi/java/android/net/wifi/SupplicantState.java b/wifi/java/android/net/wifi/SupplicantState.java
index 509b02c..4a2037d 100644
--- a/wifi/java/android/net/wifi/SupplicantState.java
+++ b/wifi/java/android/net/wifi/SupplicantState.java
@@ -171,8 +171,8 @@
     }
 
 
-    /* Supplicant associating or authenticating is considered a handshake state */
-    static boolean isHandshakeState(SupplicantState state) {
+    /** Supplicant associating or authenticating is considered a handshake state {@hide} */
+    public static boolean isHandshakeState(SupplicantState state) {
         switch(state) {
             case AUTHENTICATING:
             case ASSOCIATING:
diff --git a/wifi/java/android/net/wifi/WifiManager.java b/wifi/java/android/net/wifi/WifiManager.java
index 1a0e0da..1acfd3a 100644
--- a/wifi/java/android/net/wifi/WifiManager.java
+++ b/wifi/java/android/net/wifi/WifiManager.java
@@ -1096,7 +1096,7 @@
      * @hide
      */
      public void asyncConnect(Context srcContext, Handler srcHandler) {
-        mAsyncChannel.connect(srcContext, srcHandler, getMessenger());
+        mAsyncChannel.connect(srcContext, srcHandler, getWifiServiceMessenger());
      }
 
     /**
@@ -1197,15 +1197,30 @@
      * @return Messenger pointing to the WifiService handler
      * @hide
      */
-    public Messenger getMessenger() {
+    public Messenger getWifiServiceMessenger() {
         try {
-            return mService.getMessenger();
+            return mService.getWifiServiceMessenger();
         } catch (RemoteException e) {
             return null;
         }
     }
 
     /**
+     * Get a reference to WifiStateMachine handler.
+     * @return Messenger pointing to the WifiService handler
+     * @hide
+     */
+    public Messenger getWifiStateMachineMessenger() {
+        try {
+            return mService.getWifiStateMachineMessenger();
+        } catch (RemoteException e) {
+            return null;
+        }
+    }
+
+
+
+    /**
      * Returns the file in which IP and proxy configuration data is stored
      * @hide
      */
diff --git a/wifi/java/android/net/wifi/WifiStateMachine.java b/wifi/java/android/net/wifi/WifiStateMachine.java
index 1b64f3e..e140d80 100644
--- a/wifi/java/android/net/wifi/WifiStateMachine.java
+++ b/wifi/java/android/net/wifi/WifiStateMachine.java
@@ -463,8 +463,12 @@
     private State mScanModeState = new ScanModeState();
     /* Connecting to an access point */
     private State mConnectModeState = new ConnectModeState();
-    /* Fetching IP after network connection (assoc+auth complete) */
-    private State mConnectingState = new ConnectingState();
+    /* Connected at 802.11 (L2) level */
+    private State mL2ConnectedState = new L2ConnectedState();
+    /* fetching IP after connection to access point (assoc+auth complete) */
+    private State mObtainingIpState = new ObtainingIpState();
+    /* Waiting for link quality verification to be complete */
+    private State mVerifyingLinkState = new VerifyingLinkState();
     /* Connected with IP addr */
     private State mConnectedState = new ConnectedState();
     /* disconnect issued, waiting for network disconnect confirmation */
@@ -629,8 +633,10 @@
                 addState(mDriverStartedState, mSupplicantStartedState);
                     addState(mScanModeState, mDriverStartedState);
                     addState(mConnectModeState, mDriverStartedState);
-                        addState(mConnectingState, mConnectModeState);
-                        addState(mConnectedState, mConnectModeState);
+                        addState(mL2ConnectedState, mConnectModeState);
+                            addState(mObtainingIpState, mL2ConnectedState);
+                            addState(mVerifyingLinkState, mL2ConnectedState);
+                            addState(mConnectedState, mL2ConnectedState);
                         addState(mDisconnectingState, mConnectModeState);
                         addState(mDisconnectedState, mConnectModeState);
                         addState(mWaitForWpsCompletionState, mConnectModeState);
@@ -655,6 +661,9 @@
      * Methods exposed for public use
      ********************************************************/
 
+    public Messenger getMessenger() {
+        return new Messenger(getHandler());
+    }
     /**
      * TODO: doc
      */
@@ -1543,12 +1552,14 @@
         Intent intent = new Intent(WifiManager.NETWORK_STATE_CHANGED_ACTION);
         intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT
                 | Intent.FLAG_RECEIVER_REPLACE_PENDING);
-        intent.putExtra(WifiManager.EXTRA_NETWORK_INFO, mNetworkInfo);
+        intent.putExtra(WifiManager.EXTRA_NETWORK_INFO, new NetworkInfo(mNetworkInfo));
         intent.putExtra(WifiManager.EXTRA_LINK_PROPERTIES, new LinkProperties (mLinkProperties));
         if (bssid != null)
             intent.putExtra(WifiManager.EXTRA_BSSID, bssid);
-        if (mNetworkInfo.getState() == NetworkInfo.State.CONNECTED)
+        if (mNetworkInfo.getDetailedState() == DetailedState.VERIFYING_POOR_LINK ||
+                mNetworkInfo.getDetailedState() == DetailedState.CONNECTED) {
             intent.putExtra(WifiManager.EXTRA_WIFI_INFO, new WifiInfo(mWifiInfo));
+        }
         mContext.sendStickyBroadcast(intent);
     }
 
@@ -1740,9 +1751,6 @@
             }
         } else {
             configureLinkProperties();
-            setNetworkDetailedState(DetailedState.CONNECTED);
-            mWifiConfigStore.updateStatus(mLastNetworkId, DetailedState.CONNECTED);
-            sendNetworkStateChangeBroadcast(mLastBssid);
         }
     }
 
@@ -1890,6 +1898,8 @@
                 case CMD_SET_AP_CONFIG_COMPLETED:
                 case CMD_REQUEST_AP_CONFIG:
                 case CMD_RESPONSE_AP_CONFIG:
+                case WifiWatchdogStateMachine.POOR_LINK_DETECTED:
+                case WifiWatchdogStateMachine.GOOD_LINK_DETECTED:
                     break;
                 case WifiMonitor.DRIVER_HUNG_EVENT:
                     setWifiEnabled(false);
@@ -2885,7 +2895,7 @@
                     /* send event to CM & network change broadcast */
                     setNetworkDetailedState(DetailedState.OBTAINING_IPADDR);
                     sendNetworkStateChangeBroadcast(mLastBssid);
-                    transitionTo(mConnectingState);
+                    transitionTo(mObtainingIpState);
                     break;
                 case WifiMonitor.NETWORK_DISCONNECTION_EVENT:
                     if (DBG) log("Network connection lost");
@@ -2900,122 +2910,18 @@
         }
     }
 
-    class ConnectingState extends State {
 
-        @Override
-        public void enter() {
-            if (DBG) log(getName() + "\n");
-            EventLog.writeEvent(EVENTLOG_WIFI_STATE_CHANGED, getName());
-
-            try {
-                mNwService.enableIpv6(mInterfaceName);
-            } catch (RemoteException re) {
-                loge("Failed to enable IPv6: " + re);
-            } catch (IllegalStateException e) {
-                loge("Failed to enable IPv6: " + e);
-            }
-
-            if (!mWifiConfigStore.isUsingStaticIp(mLastNetworkId)) {
-                //start DHCP
-                mDhcpStateMachine = DhcpStateMachine.makeDhcpStateMachine(
-                        mContext, WifiStateMachine.this, mInterfaceName);
-                mDhcpStateMachine.registerForPreDhcpNotification();
-                mDhcpStateMachine.sendMessage(DhcpStateMachine.CMD_START_DHCP);
-            } else {
-                DhcpInfoInternal dhcpInfoInternal = mWifiConfigStore.getIpConfiguration(
-                        mLastNetworkId);
-                InterfaceConfiguration ifcg = new InterfaceConfiguration();
-                ifcg.setLinkAddress(dhcpInfoInternal.makeLinkAddress());
-                ifcg.setInterfaceUp();
-                try {
-                    mNwService.setInterfaceConfig(mInterfaceName, ifcg);
-                    if (DBG) log("Static IP configuration succeeded");
-                    sendMessage(CMD_STATIC_IP_SUCCESS, dhcpInfoInternal);
-                } catch (RemoteException re) {
-                    loge("Static IP configuration failed: " + re);
-                    sendMessage(CMD_STATIC_IP_FAILURE);
-                } catch (IllegalStateException e) {
-                    loge("Static IP configuration failed: " + e);
-                    sendMessage(CMD_STATIC_IP_FAILURE);
-                }
-            }
-        }
-      @Override
-      public boolean processMessage(Message message) {
-          if (DBG) log(getName() + message.toString() + "\n");
-
-          switch(message.what) {
-              case DhcpStateMachine.CMD_PRE_DHCP_ACTION:
-                  handlePreDhcpSetup();
-                  mDhcpStateMachine.sendMessage(DhcpStateMachine.CMD_PRE_DHCP_ACTION_COMPLETE);
-                  break;
-              case DhcpStateMachine.CMD_POST_DHCP_ACTION:
-                  handlePostDhcpSetup();
-                  if (message.arg1 == DhcpStateMachine.DHCP_SUCCESS) {
-                      handleSuccessfulIpConfiguration((DhcpInfoInternal) message.obj);
-                      transitionTo(mConnectedState);
-                  } else if (message.arg1 == DhcpStateMachine.DHCP_FAILURE) {
-                      handleFailedIpConfiguration();
-                      transitionTo(mDisconnectingState);
-                  }
-                  break;
-              case CMD_STATIC_IP_SUCCESS:
-                  handleSuccessfulIpConfiguration((DhcpInfoInternal) message.obj);
-                  transitionTo(mConnectedState);
-                  break;
-              case CMD_STATIC_IP_FAILURE:
-                  handleFailedIpConfiguration();
-                  transitionTo(mDisconnectingState);
-                  break;
-              case CMD_DISCONNECT:
-                  mWifiNative.disconnect();
-                  transitionTo(mDisconnectingState);
-                  break;
-                  /* Ignore connection to same network */
-              case CMD_CONNECT_NETWORK:
-                  int netId = message.arg1;
-                  if (mWifiInfo.getNetworkId() == netId) {
-                      break;
-                  }
-                  return NOT_HANDLED;
-              case CMD_SAVE_NETWORK:
-                  deferMessage(message);
-                  break;
-                  /* Ignore */
-              case WifiMonitor.NETWORK_CONNECTION_EVENT:
-                  break;
-              case CMD_SET_SCAN_MODE:
-                  if (message.arg1 == SCAN_ONLY_MODE) {
-                      sendMessage(CMD_DISCONNECT);
-                      deferMessage(message);
-                  }
-                  break;
-                  /* Defer scan when IP is being fetched */
-              case CMD_START_SCAN:
-                  deferMessage(message);
-                  break;
-                  /* Defer any power mode changes since we must keep active power mode at DHCP */
-              case CMD_SET_HIGH_PERF_MODE:
-                  deferMessage(message);
-                  break;
-              default:
-                return NOT_HANDLED;
-          }
-          EventLog.writeEvent(EVENTLOG_WIFI_EVENT_HANDLED, message.what);
-          return HANDLED;
-      }
-    }
-
-    class ConnectedState extends State {
+    class L2ConnectedState extends State {
         @Override
         public void enter() {
             if (DBG) log(getName() + "\n");
             EventLog.writeEvent(EVENTLOG_WIFI_STATE_CHANGED, getName());
             mRssiPollToken++;
             if (mEnableRssiPolling) {
-                sendMessage(obtainMessage(WifiStateMachine.CMD_RSSI_POLL, mRssiPollToken, 0));
+                sendMessage(obtainMessage(CMD_RSSI_POLL, mRssiPollToken, 0));
             }
         }
+
         @Override
         public boolean processMessage(Message message) {
             if (DBG) log(getName() + message.toString() + "\n");
@@ -3028,8 +2934,11 @@
               case DhcpStateMachine.CMD_POST_DHCP_ACTION:
                   handlePostDhcpSetup();
                   if (message.arg1 == DhcpStateMachine.DHCP_SUCCESS) {
+                      if (DBG) log("DHCP successful");
                       handleSuccessfulIpConfiguration((DhcpInfoInternal) message.obj);
+                      transitionTo(mVerifyingLinkState);
                   } else if (message.arg1 == DhcpStateMachine.DHCP_FAILURE) {
+                      if (DBG) log("DHCP failed");
                       handleFailedIpConfiguration();
                       transitionTo(mDisconnectingState);
                   }
@@ -3067,7 +2976,7 @@
                     if (mWifiInfo.getNetworkId() == result.getNetworkId()) {
                         if (result.hasIpChanged()) {
                             log("Reconfiguring IP on connection");
-                            transitionTo(mConnectingState);
+                            transitionTo(mObtainingIpState);
                         }
                         if (result.hasProxyChanged()) {
                             log("Reconfiguring proxy on connection");
@@ -3084,7 +2993,7 @@
                     if (message.arg1 == mRssiPollToken) {
                         // Get Info and continue polling
                         fetchRssiAndLinkSpeedNative();
-                        sendMessageDelayed(obtainMessage(WifiStateMachine.CMD_RSSI_POLL,
+                        sendMessageDelayed(obtainMessage(CMD_RSSI_POLL,
                                 mRssiPollToken, 0), POLL_RSSI_INTERVAL_MSECS);
                     } else {
                         // Polling has completed
@@ -3096,25 +3005,22 @@
                     if (mEnableRssiPolling) {
                         // first poll
                         fetchRssiAndLinkSpeedNative();
-                        sendMessageDelayed(obtainMessage(WifiStateMachine.CMD_RSSI_POLL,
+                        sendMessageDelayed(obtainMessage(CMD_RSSI_POLL,
                                 mRssiPollToken, 0), POLL_RSSI_INTERVAL_MSECS);
                     }
                     break;
                 default:
                     return NOT_HANDLED;
             }
+
             if (eventLoggingEnabled) {
                 EventLog.writeEvent(EVENTLOG_WIFI_EVENT_HANDLED, message.what);
             }
             return HANDLED;
         }
+
         @Override
         public void exit() {
-
-            /* Request a CS wakelock during transition to mobile */
-            checkAndSetConnectivityInstance();
-            mCm.requestNetworkTransitionWakelock(TAG);
-
             /* If a scan result is pending in connected state, the supplicant
              * is in SCAN_ONLY_MODE. Restore CONNECT_MODE on exit
              */
@@ -3124,6 +3030,141 @@
         }
     }
 
+    class ObtainingIpState extends State {
+        @Override
+        public void enter() {
+            if (DBG) log(getName() + "\n");
+            EventLog.writeEvent(EVENTLOG_WIFI_STATE_CHANGED, getName());
+
+            if (!mWifiConfigStore.isUsingStaticIp(mLastNetworkId)) {
+                //start DHCP
+                mDhcpStateMachine = DhcpStateMachine.makeDhcpStateMachine(
+                        mContext, WifiStateMachine.this, mInterfaceName);
+                mDhcpStateMachine.registerForPreDhcpNotification();
+                mDhcpStateMachine.sendMessage(DhcpStateMachine.CMD_START_DHCP);
+            } else {
+                DhcpInfoInternal dhcpInfoInternal = mWifiConfigStore.getIpConfiguration(
+                        mLastNetworkId);
+                InterfaceConfiguration ifcg = new InterfaceConfiguration();
+                ifcg.setLinkAddress(dhcpInfoInternal.makeLinkAddress());
+                ifcg.setInterfaceUp();
+                try {
+                    mNwService.setInterfaceConfig(mInterfaceName, ifcg);
+                    if (DBG) log("Static IP configuration succeeded");
+                    sendMessage(CMD_STATIC_IP_SUCCESS, dhcpInfoInternal);
+                } catch (RemoteException re) {
+                    loge("Static IP configuration failed: " + re);
+                    sendMessage(CMD_STATIC_IP_FAILURE);
+                } catch (IllegalStateException e) {
+                    loge("Static IP configuration failed: " + e);
+                    sendMessage(CMD_STATIC_IP_FAILURE);
+                }
+            }
+        }
+      @Override
+      public boolean processMessage(Message message) {
+          if (DBG) log(getName() + message.toString() + "\n");
+          switch(message.what) {
+            case CMD_STATIC_IP_SUCCESS:
+                  handleSuccessfulIpConfiguration((DhcpInfoInternal) message.obj);
+                  transitionTo(mVerifyingLinkState);
+                  break;
+              case CMD_STATIC_IP_FAILURE:
+                  handleFailedIpConfiguration();
+                  transitionTo(mDisconnectingState);
+                  break;
+             case CMD_SAVE_NETWORK:
+                  deferMessage(message);
+                  break;
+                  /* Defer any power mode changes since we must keep active power mode at DHCP */
+              case CMD_SET_HIGH_PERF_MODE:
+                  deferMessage(message);
+                  break;
+              default:
+                  return NOT_HANDLED;
+          }
+          EventLog.writeEvent(EVENTLOG_WIFI_EVENT_HANDLED, message.what);
+          return HANDLED;
+      }
+    }
+
+    class VerifyingLinkState extends State {
+        @Override
+        public void enter() {
+            if (DBG) log(getName() + "\n");
+            EventLog.writeEvent(EVENTLOG_WIFI_STATE_CHANGED, getName());
+            setNetworkDetailedState(DetailedState.VERIFYING_POOR_LINK);
+            mWifiConfigStore.updateStatus(mLastNetworkId, DetailedState.VERIFYING_POOR_LINK);
+            sendNetworkStateChangeBroadcast(mLastBssid);
+        }
+        @Override
+        public boolean processMessage(Message message) {
+            switch (message.what) {
+                case WifiWatchdogStateMachine.POOR_LINK_DETECTED:
+                    //stay here
+                    break;
+                case WifiWatchdogStateMachine.GOOD_LINK_DETECTED:
+                    try {
+                        mNwService.enableIpv6(mInterfaceName);
+                    } catch (RemoteException re) {
+                        loge("Failed to enable IPv6: " + re);
+                    } catch (IllegalStateException e) {
+                        loge("Failed to enable IPv6: " + e);
+                    }
+
+                    setNetworkDetailedState(DetailedState.CONNECTED);
+                    mWifiConfigStore.updateStatus(mLastNetworkId, DetailedState.CONNECTED);
+                    sendNetworkStateChangeBroadcast(mLastBssid);
+                    transitionTo(mConnectedState);
+                    break;
+                default:
+                    return NOT_HANDLED;
+            }
+            EventLog.writeEvent(EVENTLOG_WIFI_EVENT_HANDLED, message.what);
+            return HANDLED;
+        }
+    }
+
+    class ConnectedState extends State {
+        @Override
+        public void enter() {
+            if (DBG) log(getName() + "\n");
+            EventLog.writeEvent(EVENTLOG_WIFI_STATE_CHANGED, getName());
+       }
+        @Override
+        public boolean processMessage(Message message) {
+            if (DBG) log(getName() + message.toString() + "\n");
+            switch (message.what) {
+               case WifiWatchdogStateMachine.POOR_LINK_DETECTED:
+                    if (DBG) log("Watchdog reports poor link");
+                    try {
+                        mNwService.disableIpv6(mInterfaceName);
+                    } catch (RemoteException re) {
+                        loge("Failed to disable IPv6: " + re);
+                    } catch (IllegalStateException e) {
+                        loge("Failed to disable IPv6: " + e);
+                    }
+                    /* Report a disconnect */
+                    setNetworkDetailedState(DetailedState.DISCONNECTED);
+                    mWifiConfigStore.updateStatus(mLastNetworkId, DetailedState.DISCONNECTED);
+                    sendNetworkStateChangeBroadcast(mLastBssid);
+
+                    transitionTo(mVerifyingLinkState);
+                    break;
+                default:
+                    return NOT_HANDLED;
+            }
+            EventLog.writeEvent(EVENTLOG_WIFI_EVENT_HANDLED, message.what);
+            return HANDLED;
+        }
+        @Override
+        public void exit() {
+            /* Request a CS wakelock during transition to mobile */
+            checkAndSetConnectivityInstance();
+            mCm.requestNetworkTransitionWakelock(TAG);
+        }
+    }
+
     class DisconnectingState extends State {
         @Override
         public void enter() {
diff --git a/wifi/java/android/net/wifi/WifiWatchdogStateMachine.java b/wifi/java/android/net/wifi/WifiWatchdogStateMachine.java
index 0ca3852..a2f6343 100644
--- a/wifi/java/android/net/wifi/WifiWatchdogStateMachine.java
+++ b/wifi/java/android/net/wifi/WifiWatchdogStateMachine.java
@@ -26,9 +26,12 @@
 import android.content.IntentFilter;
 import android.content.res.Resources;
 import android.database.ContentObserver;
+import android.net.arp.ArpPeer;
 import android.net.ConnectivityManager;
-import android.net.DnsPinger;
+import android.net.LinkAddress;
+import android.net.LinkProperties;
 import android.net.NetworkInfo;
+import android.net.RouteInfo;
 import android.net.Uri;
 import android.os.Message;
 import android.os.SystemClock;
@@ -38,6 +41,7 @@
 import android.util.Log;
 
 import com.android.internal.R;
+import com.android.internal.util.AsyncChannel;
 import com.android.internal.util.Protocol;
 import com.android.internal.util.State;
 import com.android.internal.util.StateMachine;
@@ -46,49 +50,66 @@
 import java.io.PrintWriter;
 import java.net.HttpURLConnection;
 import java.net.InetAddress;
+import java.net.SocketException;
 import java.net.URL;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.List;
 
 /**
- * {@link WifiWatchdogStateMachine} monitors the initial connection to a Wi-Fi
- * network with multiple access points. After the framework successfully
- * connects to an access point, the watchdog verifies connectivity by 'pinging'
- * the configured DNS server using {@link DnsPinger}.
- * <p>
- * On DNS check failure, the BSSID is blacklisted if it is reasonably likely
- * that another AP might have internet access; otherwise the SSID is disabled.
- * <p>
- * On DNS success, the WatchdogService initiates a walled garden check via an
- * http get. A browser window is activated if a walled garden is detected.
+ * WifiWatchdogStateMachine monitors the connection to a Wi-Fi
+ * network. After the framework notifies that it has connected to an
+ * acccess point and is waiting for link to be verified, the watchdog
+ * takes over and verifies if the link is good by doing ARP pings to
+ * the gateway using {@link ArpPeer}.
+ *
+ * Upon successful verification, the watchdog notifies and continues
+ * to monitor the link afterwards when the RSSI level falls below
+ * a certain threshold.
+
+ * When Wi-fi connects at L2 layer, the beacons from access point reach
+ * the device and it can maintain a connection, but the application
+ * connectivity can be flaky (due to bigger packet size exchange).
+ *
+ * We now monitor the quality of the last hop on
+ * Wi-Fi using signal strength and ARP connectivity as indicators
+ * to decide if the link is good enough to switch to Wi-Fi as the uplink.
+ *
+ * ARP pings are useful for link validation but can still get through
+ * when the application traffic fails to go through and are thus not
+ * the best indicator of real packet loss since they are tiny packets
+ * (28 bytes) and have a much low chance of packet corruption than the
+ * regular data packets.
+ *
+ * When signal strength and ARP are used together, it ends up working well in tests.
+ * The goal is to switch to Wi-Fi after validating ARP transfer
+ * and RSSI and then switching out of Wi-Fi when we hit a low
+ * signal strength threshold and then waiting until the signal strength
+ * improves and validating ARP transfer.
  *
  * @hide
  */
 public class WifiWatchdogStateMachine extends StateMachine {
 
-    private static final boolean DBG = false;
+    /* STOPSHIP: Keep this configurable for debugging until ship */
+    private static boolean DBG = false;
     private static final String TAG = "WifiWatchdogStateMachine";
-    private static final String DISABLED_NETWORK_NOTIFICATION_ID = "WifiWatchdog.networkdisabled";
     private static final String WALLED_GARDEN_NOTIFICATION_ID = "WifiWatchdog.walledgarden";
 
-    private static final int WIFI_SIGNAL_LEVELS = 4;
-    /**
-     * Low signal is defined as less than or equal to cut off
-     */
-    private static final int LOW_SIGNAL_CUTOFF = 0;
+    /* Wi-fi connection is considered poor below this
+       RSSI level threshold and the watchdog report it
+       to the WifiStateMachine */
+    private static final int RSSI_LEVEL_CUTOFF = 1;
+    /* Wi-fi connection is monitored actively below this
+       threshold */
+    private static final int RSSI_LEVEL_MONITOR = 2;
 
-    private static final long DEFAULT_DNS_CHECK_SHORT_INTERVAL_MS = 2 * 60 * 1000;
-    private static final long DEFAULT_DNS_CHECK_LONG_INTERVAL_MS = 60 * 60 * 1000;
+    private int mCurrentSignalLevel;
+
+    private static final long DEFAULT_ARP_CHECK_INTERVAL_MS = 2 * 60 * 1000;
     private static final long DEFAULT_WALLED_GARDEN_INTERVAL_MS = 30 * 60 * 1000;
 
-    private static final int DEFAULT_MAX_SSID_BLACKLISTS = 7;
-    private static final int DEFAULT_NUM_DNS_PINGS = 5; // Multiple pings to detect setup issues
-    private static final int DEFAULT_MIN_DNS_RESPONSES = 1;
+    private static final int DEFAULT_NUM_ARP_PINGS = 5;
+    private static final int DEFAULT_MIN_ARP_RESPONSES = 1;
 
-    private static final int DEFAULT_DNS_PING_TIMEOUT_MS = 2000;
-
-    private static final long DEFAULT_BLACKLIST_FOLLOWUP_INTERVAL_MS = 15 * 1000;
+    private static final int DEFAULT_ARP_PING_TIMEOUT_MS = 100;
 
     // See http://go/clientsdns for usage approval
     private static final String DEFAULT_WALLED_GARDEN_URL =
@@ -102,10 +123,6 @@
      */
     private static final int WALLED_GARDEN_START_DELAY_MS = 3000;
 
-    private static final int DNS_INTRATEST_PING_INTERVAL_MS = 200;
-    /* With some router setups, it takes a few hunder milli-seconds before connection is active */
-    private static final int DNS_START_DELAY_MS = 1000;
-
     private static final int BASE = Protocol.BASE_WIFI_WATCHDOG;
 
     /**
@@ -118,99 +135,76 @@
      * which has a non-null networkInfo object
      */
     private static final int EVENT_NETWORK_STATE_CHANGE             = BASE + 2;
-    /**
-     * Indicates the signal has changed. Passed with arg1
-     * {@link #mNetEventCounter} and arg2 [raw signal strength]
-     */
+    /* Passed with RSSI information */
     private static final int EVENT_RSSI_CHANGE                      = BASE + 3;
-    private static final int EVENT_SCAN_RESULTS_AVAILABLE           = BASE + 4;
     private static final int EVENT_WIFI_RADIO_STATE_CHANGE          = BASE + 5;
     private static final int EVENT_WATCHDOG_SETTINGS_CHANGE         = BASE + 6;
 
-    private static final int MESSAGE_HANDLE_WALLED_GARDEN           = BASE + 100;
-    private static final int MESSAGE_HANDLE_BAD_AP                  = BASE + 101;
-    /**
-     * arg1 == mOnlineWatchState.checkCount
-     */
-    private static final int MESSAGE_SINGLE_DNS_CHECK               = BASE + 102;
-    private static final int MESSAGE_NETWORK_FOLLOWUP               = BASE + 103;
-    private static final int MESSAGE_DELAYED_WALLED_GARDEN_CHECK    = BASE + 104;
+    /* Internal messages */
+    private static final int CMD_ARP_CHECK                          = BASE + 11;
+    private static final int CMD_DELAYED_WALLED_GARDEN_CHECK        = BASE + 12;
+
+    /* Notifications to WifiStateMachine */
+    static final int POOR_LINK_DETECTED                             = BASE + 21;
+    static final int GOOD_LINK_DETECTED                             = BASE + 22;
+
+    private static final int SINGLE_ARP_CHECK = 0;
+    private static final int FULL_ARP_CHECK   = 1;
 
     private Context mContext;
     private ContentResolver mContentResolver;
     private WifiManager mWifiManager;
-    private DnsPinger mDnsPinger;
     private IntentFilter mIntentFilter;
     private BroadcastReceiver mBroadcastReceiver;
+    private AsyncChannel mWsmChannel = new AsyncChannel();;
 
     private DefaultState mDefaultState = new DefaultState();
     private WatchdogDisabledState mWatchdogDisabledState = new WatchdogDisabledState();
     private WatchdogEnabledState mWatchdogEnabledState = new WatchdogEnabledState();
     private NotConnectedState mNotConnectedState = new NotConnectedState();
+    private VerifyingLinkState mVerifyingLinkState = new VerifyingLinkState();
     private ConnectedState mConnectedState = new ConnectedState();
-    private DnsCheckingState mDnsCheckingState = new DnsCheckingState();
+    private WalledGardenCheckState mWalledGardenCheckState = new WalledGardenCheckState();
+    /* Online and watching link connectivity */
     private OnlineWatchState mOnlineWatchState = new OnlineWatchState();
+    /* Online and doing nothing */
     private OnlineState mOnlineState = new OnlineState();
-    private DnsCheckFailureState mDnsCheckFailureState = new DnsCheckFailureState();
-    private DelayWalledGardenState mDelayWalledGardenState = new DelayWalledGardenState();
-    private WalledGardenState mWalledGardenState = new WalledGardenState();
-    private BlacklistedApState mBlacklistedApState = new BlacklistedApState();
 
-    private long mDnsCheckShortIntervalMs;
-    private long mDnsCheckLongIntervalMs;
+    private int mArpToken = 0;
+    private long mArpCheckIntervalMs;
     private long mWalledGardenIntervalMs;
-    private int mMaxSsidBlacklists;
-    private int mNumDnsPings;
-    private int mMinDnsResponses;
-    private int mDnsPingTimeoutMs;
-    private long mBlacklistFollowupIntervalMs;
+    private int mNumArpPings;
+    private int mMinArpResponses;
+    private int mArpPingTimeoutMs;
     private boolean mPoorNetworkDetectionEnabled;
     private boolean mWalledGardenTestEnabled;
     private String mWalledGardenUrl;
 
-    private boolean mShowDisabledNotification;
-    /**
-     * The {@link WifiInfo} object passed to WWSM on network broadcasts
-     */
-    private WifiInfo mConnectionInfo;
-    private int mNetEventCounter = 0;
+    private WifiInfo mWifiInfo;
+    private LinkProperties mLinkProperties;
 
-    /**
-     * Currently maintained but not used, TODO
-     */
-    private HashSet<String> mBssids = new HashSet<String>();
-    private int mNumCheckFailures = 0;
+    private long mLastWalledGardenCheckTime = 0;
 
-    private Long mLastWalledGardenCheckTime = null;
-
-    /**
-     * This is set by the blacklisted state and reset when connected to a new AP.
-     * It triggers a disableNetwork call if a DNS check fails.
-     */
-    public boolean mDisableAPNextFailure = false;
     private static boolean sWifiOnly = false;
-    private boolean mDisabledNotificationShown;
     private boolean mWalledGardenNotificationShown;
-    public boolean mHasConnectedWifiManager = false;
 
     /**
      * STATE MAP
      *          Default
      *         /       \
-     * Disabled     Enabled
-     *             /       \
-     * NotConnected      Connected
-     *                  /---------\
-     *               (all other states)
+     * Disabled      Enabled
+     *             /     \     \
+     * NotConnected  Verifying  Connected
+     *                         /---------\
+     *                       (all other states)
      */
     private WifiWatchdogStateMachine(Context context) {
         super(TAG);
         mContext = context;
         mContentResolver = context.getContentResolver();
         mWifiManager = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
-        mDnsPinger = new DnsPinger(mContext, "WifiWatchdogStateMachine.DnsPinger",
-                                this.getHandler().getLooper(), this.getHandler(),
-                                ConnectivityManager.TYPE_WIFI);
+        mWsmChannel.connectSync(mContext, getHandler(),
+                mWifiManager.getWifiStateMachineMessenger());
 
         setupNetworkReceiver();
 
@@ -221,16 +215,17 @@
             addState(mWatchdogDisabledState, mDefaultState);
             addState(mWatchdogEnabledState, mDefaultState);
                 addState(mNotConnectedState, mWatchdogEnabledState);
+                addState(mVerifyingLinkState, mWatchdogEnabledState);
                 addState(mConnectedState, mWatchdogEnabledState);
-                    addState(mDnsCheckingState, mConnectedState);
-                    addState(mDnsCheckFailureState, mConnectedState);
-                    addState(mDelayWalledGardenState, mConnectedState);
-                    addState(mWalledGardenState, mConnectedState);
-                    addState(mBlacklistedApState, mConnectedState);
+                    addState(mWalledGardenCheckState, mConnectedState);
                     addState(mOnlineWatchState, mConnectedState);
                     addState(mOnlineState, mConnectedState);
 
-        setInitialState(mWatchdogDisabledState);
+        if (isWatchdogEnabled()) {
+            setInitialState(mNotConnectedState);
+        } else {
+            setInitialState(mWatchdogDisabledState);
+        }
         updateSettings();
     }
 
@@ -242,19 +237,15 @@
         sWifiOnly = (cm.isNetworkSupported(ConnectivityManager.TYPE_MOBILE) == false);
 
         // Disable for wifi only devices.
-        if (Settings.Secure.getString(contentResolver, Settings.Secure.WIFI_WATCHDOG_ON) == null &&
-                sWifiOnly) {
+        if (Settings.Secure.getString(contentResolver, Settings.Secure.WIFI_WATCHDOG_ON) == null
+                && sWifiOnly) {
             putSettingsBoolean(contentResolver, Settings.Secure.WIFI_WATCHDOG_ON, false);
         }
         WifiWatchdogStateMachine wwsm = new WifiWatchdogStateMachine(context);
         wwsm.start();
-        wwsm.sendMessage(EVENT_WATCHDOG_TOGGLED);
         return wwsm;
     }
 
-    /**
-   *
-   */
     private void setupNetworkReceiver() {
         mBroadcastReceiver = new BroadcastReceiver() {
             @Override
@@ -263,10 +254,8 @@
                 if (action.equals(WifiManager.NETWORK_STATE_CHANGED_ACTION)) {
                     sendMessage(EVENT_NETWORK_STATE_CHANGE, intent);
                 } else if (action.equals(WifiManager.RSSI_CHANGED_ACTION)) {
-                    obtainMessage(EVENT_RSSI_CHANGE, mNetEventCounter,
-                            intent.getIntExtra(WifiManager.EXTRA_NEW_RSSI, -200)).sendToTarget();
-                } else if (action.equals(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION)) {
-                    sendMessage(EVENT_SCAN_RESULTS_AVAILABLE);
+                    obtainMessage(EVENT_RSSI_CHANGE,
+                            intent.getIntExtra(WifiManager.EXTRA_NEW_RSSI, -200), 0).sendToTarget();
                 } else if (action.equals(WifiManager.WIFI_STATE_CHANGED_ACTION)) {
                     sendMessage(EVENT_WIFI_RADIO_STATE_CHANGE,
                             intent.getIntExtra(WifiManager.EXTRA_WIFI_STATE,
@@ -279,7 +268,7 @@
         mIntentFilter.addAction(WifiManager.NETWORK_STATE_CHANGED_ACTION);
         mIntentFilter.addAction(WifiManager.WIFI_STATE_CHANGED_ACTION);
         mIntentFilter.addAction(WifiManager.RSSI_CHANGED_ACTION);
-        mIntentFilter.addAction(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION);
+        mContext.registerReceiver(mBroadcastReceiver, mIntentFilter);
     }
 
     /**
@@ -311,39 +300,29 @@
 
         mContext.getContentResolver().registerContentObserver(
                 Settings.Secure.getUriFor(
-                        Settings.Secure.WIFI_WATCHDOG_DNS_CHECK_SHORT_INTERVAL_MS),
+                        Settings.Secure.WIFI_WATCHDOG_ARP_CHECK_INTERVAL_MS),
                         false, contentObserver);
         mContext.getContentResolver().registerContentObserver(
-                Settings.Secure.getUriFor(Settings.Secure.WIFI_WATCHDOG_DNS_CHECK_LONG_INTERVAL_MS),
-                false, contentObserver);
-        mContext.getContentResolver().registerContentObserver(
                 Settings.Secure.getUriFor(Settings.Secure.WIFI_WATCHDOG_WALLED_GARDEN_INTERVAL_MS),
                 false, contentObserver);
         mContext.getContentResolver().registerContentObserver(
-                Settings.Secure.getUriFor(Settings.Secure.WIFI_WATCHDOG_MAX_SSID_BLACKLISTS),
+                Settings.Secure.getUriFor(Settings.Secure.WIFI_WATCHDOG_NUM_ARP_PINGS),
                 false, contentObserver);
         mContext.getContentResolver().registerContentObserver(
-                Settings.Secure.getUriFor(Settings.Secure.WIFI_WATCHDOG_NUM_DNS_PINGS),
+                Settings.Secure.getUriFor(Settings.Secure.WIFI_WATCHDOG_MIN_ARP_RESPONSES),
                 false, contentObserver);
         mContext.getContentResolver().registerContentObserver(
-                Settings.Secure.getUriFor(Settings.Secure.WIFI_WATCHDOG_MIN_DNS_RESPONSES),
+                Settings.Secure.getUriFor(Settings.Secure.WIFI_WATCHDOG_ARP_PING_TIMEOUT_MS),
                 false, contentObserver);
         mContext.getContentResolver().registerContentObserver(
-                Settings.Secure.getUriFor(Settings.Secure.WIFI_WATCHDOG_DNS_PING_TIMEOUT_MS),
+                Settings.Secure.getUriFor(Settings.Secure.WIFI_WATCHDOG_POOR_NETWORK_TEST_ENABLED),
                 false, contentObserver);
         mContext.getContentResolver().registerContentObserver(
-                Settings.Secure.getUriFor(
-                        Settings.Secure.WIFI_WATCHDOG_BLACKLIST_FOLLOWUP_INTERVAL_MS),
-                        false, contentObserver);
-        mContext.getContentResolver().registerContentObserver(
                 Settings.Secure.getUriFor(Settings.Secure.WIFI_WATCHDOG_WALLED_GARDEN_TEST_ENABLED),
                 false, contentObserver);
         mContext.getContentResolver().registerContentObserver(
                 Settings.Secure.getUriFor(Settings.Secure.WIFI_WATCHDOG_WALLED_GARDEN_URL),
                 false, contentObserver);
-        mContext.getContentResolver().registerContentObserver(
-                Settings.Secure.getUriFor(Settings.Secure.WIFI_WATCHDOG_SHOW_DISABLED_NETWORK_POPUP)
-                , false, contentObserver);
     }
 
     /**
@@ -375,17 +354,20 @@
         }
     }
 
-    private boolean rssiStrengthAboveCutoff(int rssi) {
-        return WifiManager.calculateSignalLevel(rssi, WIFI_SIGNAL_LEVELS) > LOW_SIGNAL_CUTOFF;
-    }
-
     public void dump(PrintWriter pw) {
         pw.print("WatchdogStatus: ");
-        pw.print("State " + getCurrentState());
-        pw.println(", network [" + mConnectionInfo + "]");
-        pw.print("checkFailures   " + mNumCheckFailures);
-        pw.println(", bssids: " + mBssids);
-        pw.println("lastSingleCheck: " + mOnlineWatchState.lastCheckTime);
+        pw.print("State: " + getCurrentState());
+        pw.println("mWifiInfo: [" + mWifiInfo + "]");
+        pw.println("mLinkProperties: [" + mLinkProperties + "]");
+        pw.println("mCurrentSignalLevel: [" + mCurrentSignalLevel + "]");
+        pw.println("mArpCheckIntervalMs: [" + mArpCheckIntervalMs+ "]");
+        pw.println("mWalledGardenIntervalMs: [" + mWalledGardenIntervalMs + "]");
+        pw.println("mNumArpPings: [" + mNumArpPings + "]");
+        pw.println("mMinArpResponses: [" + mMinArpResponses + "]");
+        pw.println("mArpPingTimeoutMs: [" + mArpPingTimeoutMs + "]");
+        pw.println("mPoorNetworkDetectionEnabled: [" + mPoorNetworkDetectionEnabled + "]");
+        pw.println("mWalledGardenTestEnabled: [" + mWalledGardenTestEnabled + "]");
+        pw.println("mWalledGardenUrl: [" + mWalledGardenUrl + "]");
     }
 
     private boolean isWatchdogEnabled() {
@@ -393,31 +375,22 @@
     }
 
     private void updateSettings() {
-        mDnsCheckShortIntervalMs = Secure.getLong(mContentResolver,
-                Secure.WIFI_WATCHDOG_DNS_CHECK_SHORT_INTERVAL_MS,
-                DEFAULT_DNS_CHECK_SHORT_INTERVAL_MS);
-        mDnsCheckLongIntervalMs = Secure.getLong(mContentResolver,
-                Secure.WIFI_WATCHDOG_DNS_CHECK_LONG_INTERVAL_MS,
-                DEFAULT_DNS_CHECK_LONG_INTERVAL_MS);
-        mMaxSsidBlacklists = Secure.getInt(mContentResolver,
-                Secure.WIFI_WATCHDOG_MAX_SSID_BLACKLISTS,
-                DEFAULT_MAX_SSID_BLACKLISTS);
-        mNumDnsPings = Secure.getInt(mContentResolver,
-                Secure.WIFI_WATCHDOG_NUM_DNS_PINGS,
-                DEFAULT_NUM_DNS_PINGS);
-        mMinDnsResponses = Secure.getInt(mContentResolver,
-                Secure.WIFI_WATCHDOG_MIN_DNS_RESPONSES,
-                DEFAULT_MIN_DNS_RESPONSES);
-        mDnsPingTimeoutMs = Secure.getInt(mContentResolver,
-                Secure.WIFI_WATCHDOG_DNS_PING_TIMEOUT_MS,
-                DEFAULT_DNS_PING_TIMEOUT_MS);
-        mBlacklistFollowupIntervalMs = Secure.getLong(mContentResolver,
-                Settings.Secure.WIFI_WATCHDOG_BLACKLIST_FOLLOWUP_INTERVAL_MS,
-                DEFAULT_BLACKLIST_FOLLOWUP_INTERVAL_MS);
-        //TODO: enable this by default after changing watchdog behavior
-        //Also, update settings description
+        if (DBG) log("Updating secure settings");
+
+        mArpCheckIntervalMs = Secure.getLong(mContentResolver,
+                Secure.WIFI_WATCHDOG_ARP_CHECK_INTERVAL_MS,
+                DEFAULT_ARP_CHECK_INTERVAL_MS);
+        mNumArpPings = Secure.getInt(mContentResolver,
+                Secure.WIFI_WATCHDOG_NUM_ARP_PINGS,
+                DEFAULT_NUM_ARP_PINGS);
+        mMinArpResponses = Secure.getInt(mContentResolver,
+                Secure.WIFI_WATCHDOG_MIN_ARP_RESPONSES,
+                DEFAULT_MIN_ARP_RESPONSES);
+        mArpPingTimeoutMs = Secure.getInt(mContentResolver,
+                Secure.WIFI_WATCHDOG_ARP_PING_TIMEOUT_MS,
+                DEFAULT_ARP_PING_TIMEOUT_MS);
         mPoorNetworkDetectionEnabled = getSettingsBoolean(mContentResolver,
-                Settings.Secure.WIFI_WATCHDOG_POOR_NETWORK_TEST_ENABLED, false);
+                Settings.Secure.WIFI_WATCHDOG_POOR_NETWORK_TEST_ENABLED, true);
         mWalledGardenTestEnabled = getSettingsBoolean(mContentResolver,
                 Settings.Secure.WIFI_WATCHDOG_WALLED_GARDEN_TEST_ENABLED, true);
         mWalledGardenUrl = getSettingsStr(mContentResolver,
@@ -426,69 +399,6 @@
         mWalledGardenIntervalMs = Secure.getLong(mContentResolver,
                 Secure.WIFI_WATCHDOG_WALLED_GARDEN_INTERVAL_MS,
                 DEFAULT_WALLED_GARDEN_INTERVAL_MS);
-        mShowDisabledNotification = getSettingsBoolean(mContentResolver,
-                Settings.Secure.WIFI_WATCHDOG_SHOW_DISABLED_NETWORK_POPUP, true);
-    }
-
-    /**
-     * Helper to return wait time left given a min interval and last run
-     *
-     * @param interval minimum wait interval
-     * @param lastTime last time action was performed in
-     *            SystemClock.elapsedRealtime(). Null if never.
-     * @return non negative time to wait
-     */
-    private static long waitTime(long interval, Long lastTime) {
-        if (lastTime == null)
-            return 0;
-        long wait = interval + lastTime - SystemClock.elapsedRealtime();
-        return wait > 0 ? wait : 0;
-    }
-
-    private static String wifiInfoToStr(WifiInfo wifiInfo) {
-        if (wifiInfo == null)
-            return "null";
-        return "(" + wifiInfo.getSSID() + ", " + wifiInfo.getBSSID() + ")";
-    }
-
-    /**
-     * Uses {@link #mConnectionInfo}.
-     */
-    private void updateBssids() {
-        String curSsid = mConnectionInfo.getSSID();
-        List<ScanResult> results = mWifiManager.getScanResults();
-        int oldNumBssids = mBssids.size();
-
-        if (results == null) {
-            if (DBG) {
-                log("updateBssids: Got null scan results!");
-            }
-            return;
-        }
-
-        for (ScanResult result : results) {
-            if (result == null || result.SSID == null) {
-                if (DBG) {
-                    log("Received invalid scan result: " + result);
-                }
-                continue;
-            }
-            if (curSsid.equals(result.SSID))
-                mBssids.add(result.BSSID);
-        }
-    }
-
-    private void resetWatchdogState() {
-        if (DBG) {
-            log("Resetting watchdog state...");
-        }
-        mConnectionInfo = null;
-        mDisableAPNextFailure = false;
-        mLastWalledGardenCheckTime = null;
-        mNumCheckFailures = 0;
-        mBssids.clear();
-        setDisabledNetworkNotificationVisible(false);
-        setWalledGardenNotificationVisible(false);
     }
 
     private void setWalledGardenNotificationVisible(boolean visible) {
@@ -507,7 +417,7 @@
 
             CharSequence title = r.getString(R.string.wifi_available_sign_in, 0);
             CharSequence details = r.getString(R.string.wifi_available_sign_in_detailed,
-                    mConnectionInfo.getSSID());
+                    mWifiInfo.getSSID());
 
             Notification notification = new Notification();
             notification.when = 0;
@@ -524,41 +434,6 @@
         mWalledGardenNotificationShown = visible;
     }
 
-    private void setDisabledNetworkNotificationVisible(boolean visible) {
-        // If it should be hidden and it is already hidden, then noop
-        if (!visible && !mDisabledNotificationShown) {
-            return;
-        }
-
-        Resources r = Resources.getSystem();
-        NotificationManager notificationManager = (NotificationManager) mContext
-            .getSystemService(Context.NOTIFICATION_SERVICE);
-
-        if (visible) {
-            CharSequence title = r.getText(R.string.wifi_watchdog_network_disabled);
-            String msg = mConnectionInfo.getSSID() +
-                r.getText(R.string.wifi_watchdog_network_disabled_detailed);
-
-            Notification wifiDisabledWarning = new Notification.Builder(mContext)
-                .setSmallIcon(R.drawable.stat_sys_warning)
-                .setDefaults(Notification.DEFAULT_ALL)
-                .setTicker(title)
-                .setContentTitle(title)
-                .setContentText(msg)
-                .setContentIntent(PendingIntent.getActivity(mContext, 0,
-                            new Intent(WifiManager.ACTION_PICK_WIFI_NETWORK)
-                            .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK), 0))
-                .setWhen(System.currentTimeMillis())
-                .setAutoCancel(true)
-                .getNotification();
-
-            notificationManager.notify(DISABLED_NETWORK_NOTIFICATION_ID, 1, wifiDisabledWarning);
-        } else {
-            notificationManager.cancel(DISABLED_NETWORK_NOTIFICATION_ID, 1);
-        }
-        mDisabledNotificationShown = visible;
-    }
-
     class DefaultState extends State {
         @Override
         public boolean processMessage(Message msg) {
@@ -568,11 +443,20 @@
                     if (DBG) {
                         log("Updating wifi-watchdog secure settings");
                     }
-                    return HANDLED;
-            }
-            if (DBG) {
-                log("Caught message " + msg.what + " in state " +
-                        getCurrentState().getName());
+                    break;
+                case EVENT_RSSI_CHANGE:
+                    mCurrentSignalLevel = WifiManager.calculateSignalLevel(msg.arg1,
+                            WifiManager.RSSI_LEVELS);
+                    break;
+                case EVENT_WIFI_RADIO_STATE_CHANGE:
+                case EVENT_NETWORK_STATE_CHANGE:
+                case CMD_ARP_CHECK:
+                case CMD_DELAYED_WALLED_GARDEN_CHECK:
+                    //ignore
+                    break;
+                default:
+                    log("Unhandled message " + msg + " in state " + getCurrentState().getName());
+                    break;
             }
             return HANDLED;
         }
@@ -586,6 +470,20 @@
                     if (isWatchdogEnabled())
                         transitionTo(mNotConnectedState);
                     return HANDLED;
+                case EVENT_NETWORK_STATE_CHANGE:
+                    Intent intent = (Intent) msg.obj;
+                    NetworkInfo networkInfo = (NetworkInfo)
+                            intent.getParcelableExtra(WifiManager.EXTRA_NETWORK_INFO);
+
+                    switch (networkInfo.getDetailedState()) {
+                        case VERIFYING_POOR_LINK:
+                            if (DBG) log("Watchdog disabled, verify link");
+                            mWsmChannel.sendMessage(GOOD_LINK_DETECTED);
+                            break;
+                        default:
+                            break;
+                    }
+                    break;
             }
             return NOT_HANDLED;
         }
@@ -594,10 +492,8 @@
     class WatchdogEnabledState extends State {
         @Override
         public void enter() {
-            resetWatchdogState();
-            mContext.registerReceiver(mBroadcastReceiver, mIntentFilter);
             if (DBG) log("WifiWatchdogService enabled");
-        }
+       }
 
         @Override
         public boolean processMessage(Message msg) {
@@ -605,77 +501,57 @@
                 case EVENT_WATCHDOG_TOGGLED:
                     if (!isWatchdogEnabled())
                         transitionTo(mWatchdogDisabledState);
-                    return HANDLED;
+                    break;
                 case EVENT_NETWORK_STATE_CHANGE:
-                    Intent stateChangeIntent = (Intent) msg.obj;
+                    Intent intent = (Intent) msg.obj;
                     NetworkInfo networkInfo = (NetworkInfo)
-                            stateChangeIntent.getParcelableExtra(WifiManager.EXTRA_NETWORK_INFO);
+                            intent.getParcelableExtra(WifiManager.EXTRA_NETWORK_INFO);
 
-                    setDisabledNetworkNotificationVisible(false);
-                    setWalledGardenNotificationVisible(false);
-                    switch (networkInfo.getState()) {
-                        case CONNECTED:
-                            WifiInfo wifiInfo = (WifiInfo)
-                                stateChangeIntent.getParcelableExtra(WifiManager.EXTRA_WIFI_INFO);
-                            if (wifiInfo == null) {
-                                loge("Connected --> WifiInfo object null!");
-                                return HANDLED;
-                            }
-
-                            if (wifiInfo.getSSID() == null || wifiInfo.getBSSID() == null) {
-                                loge("Received wifiInfo object with null elts: "
-                                        + wifiInfoToStr(wifiInfo));
-                                return HANDLED;
-                            }
-
-                            initConnection(wifiInfo);
-                            mConnectionInfo = wifiInfo;
-                            mNetEventCounter++;
+                    switch (networkInfo.getDetailedState()) {
+                        case VERIFYING_POOR_LINK:
+                            mLinkProperties = (LinkProperties) intent.getParcelableExtra(
+                                    WifiManager.EXTRA_LINK_PROPERTIES);
+                            mWifiInfo = (WifiInfo) intent.getParcelableExtra(
+                                    WifiManager.EXTRA_WIFI_INFO);
                             if (mPoorNetworkDetectionEnabled) {
-                                updateBssids();
-                                transitionTo(mDnsCheckingState);
+                                if (mWifiInfo == null) {
+                                    log("Ignoring link verification, mWifiInfo is NULL");
+                                    mWsmChannel.sendMessage(GOOD_LINK_DETECTED);
+                                } else {
+                                    transitionTo(mVerifyingLinkState);
+                                }
                             } else {
-                                transitionTo(mDelayWalledGardenState);
+                                mWsmChannel.sendMessage(GOOD_LINK_DETECTED);
+                            }
+                            break;
+                        case CONNECTED:
+                            if (shouldCheckWalledGarden()) {
+                                transitionTo(mWalledGardenCheckState);
+                            } else {
+                                transitionTo(mOnlineWatchState);
                             }
                             break;
                         default:
-                            mNetEventCounter++;
                             transitionTo(mNotConnectedState);
                             break;
                     }
-                    return HANDLED;
+                    break;
                 case EVENT_WIFI_RADIO_STATE_CHANGE:
                     if ((Integer) msg.obj == WifiManager.WIFI_STATE_DISABLING) {
                         if (DBG) log("WifiStateDisabling -- Resetting WatchdogState");
-                        resetWatchdogState();
-                        mNetEventCounter++;
                         transitionTo(mNotConnectedState);
                     }
-                    return HANDLED;
+                    break;
+                default:
+                    return NOT_HANDLED;
             }
 
-            return NOT_HANDLED;
-        }
-
-        /**
-         * @param wifiInfo Info object with non-null ssid and bssid
-         */
-        private void initConnection(WifiInfo wifiInfo) {
-            if (DBG) {
-                log("Connected:: old " + wifiInfoToStr(mConnectionInfo) +
-                        " ==> new " + wifiInfoToStr(wifiInfo));
-            }
-
-            if (mConnectionInfo == null || !wifiInfo.getSSID().equals(mConnectionInfo.getSSID())) {
-                resetWatchdogState();
-            } else if (!wifiInfo.getBSSID().equals(mConnectionInfo.getBSSID())) {
-                mDisableAPNextFailure = false;
-            }
+            setWalledGardenNotificationVisible(false);
+            return HANDLED;
         }
 
         @Override
         public void exit() {
-            mContext.unregisterReceiver(mBroadcastReceiver);
             if (DBG) log("WifiWatchdogService disabled");
         }
     }
@@ -683,17 +559,74 @@
     class NotConnectedState extends State {
     }
 
-    class ConnectedState extends State {
+    class VerifyingLinkState extends State {
+        @Override
+        public void enter() {
+            if (DBG) log(getName() + "\n");
+            //Treat entry as an rssi change
+            handleRssiChange();
+        }
+
+        private void handleRssiChange() {
+            if (mCurrentSignalLevel <= RSSI_LEVEL_CUTOFF) {
+                //stay here
+                if (DBG) log("enter VerifyingLinkState, stay level: " + mCurrentSignalLevel);
+            } else {
+                if (DBG) log("enter VerifyingLinkState, arp check level: " + mCurrentSignalLevel);
+                sendMessage(obtainMessage(CMD_ARP_CHECK, ++mArpToken, 0));
+            }
+        }
+
         @Override
         public boolean processMessage(Message msg) {
             switch (msg.what) {
-                case EVENT_SCAN_RESULTS_AVAILABLE:
-                    if (mPoorNetworkDetectionEnabled) {
-                        updateBssids();
-                    }
-                    return HANDLED;
                 case EVENT_WATCHDOG_SETTINGS_CHANGE:
                     updateSettings();
+                    if (!mPoorNetworkDetectionEnabled) {
+                        mWsmChannel.sendMessage(GOOD_LINK_DETECTED);
+                    }
+                    break;
+                case EVENT_RSSI_CHANGE:
+                    int signalLevel = WifiManager.calculateSignalLevel(msg.arg1,
+                            WifiManager.RSSI_LEVELS);
+                    if (DBG) log("RSSI change old: " + mCurrentSignalLevel + "new: " + signalLevel);
+                    mCurrentSignalLevel = signalLevel;
+
+                    handleRssiChange();
+                    break;
+                case CMD_ARP_CHECK:
+                    if (msg.arg1 == mArpToken) {
+                        if (doArpTest(FULL_ARP_CHECK) == true) {
+                            if (DBG) log("Notify link is good " + mCurrentSignalLevel);
+                            mWsmChannel.sendMessage(GOOD_LINK_DETECTED);
+                        } else {
+                            if (DBG) log("Continue ARP check, rssi level: " + mCurrentSignalLevel);
+                            sendMessageDelayed(obtainMessage(CMD_ARP_CHECK, ++mArpToken, 0),
+                                    mArpCheckIntervalMs);
+                        }
+                    }
+                    break;
+                default:
+                    return NOT_HANDLED;
+            }
+            return HANDLED;
+        }
+    }
+
+    class ConnectedState extends State {
+        @Override
+        public void enter() {
+            if (DBG) log(getName() + "\n");
+        }
+        @Override
+        public boolean processMessage(Message msg) {
+            switch (msg.what) {
+                case EVENT_WATCHDOG_SETTINGS_CHANGE:
+                    updateSettings();
+                    //STOPSHIP: Remove this at ship
+                    DBG = true;
+                    if (DBG) log("Updated secure settings and turned debug on");
+
                     if (mPoorNetworkDetectionEnabled) {
                         transitionTo(mOnlineWatchState);
                     } else {
@@ -705,402 +638,162 @@
         }
     }
 
-    class DnsCheckingState extends State {
-        List<InetAddress> mDnsList;
-        int[] dnsCheckSuccesses;
-        String dnsCheckLogStr;
-        String[] dnsResponseStrs;
-        /** Keeps track of active dns pings.  Map is from pingID to index in mDnsList */
-        HashMap<Integer, Integer> idDnsMap = new HashMap<Integer, Integer>();
-
+    class WalledGardenCheckState extends State {
+        private int mWalledGardenToken = 0;
         @Override
         public void enter() {
-            mDnsList = mDnsPinger.getDnsList();
-            int numDnses = mDnsList.size();
-            dnsCheckSuccesses = new int[numDnses];
-            dnsResponseStrs = new String[numDnses];
-            for (int i = 0; i < numDnses; i++)
-                dnsResponseStrs[i] = "";
-
-            if (DBG) {
-                dnsCheckLogStr = String.format("Pinging %s on ssid [%s]: ",
-                        mDnsList, mConnectionInfo.getSSID());
-                log(dnsCheckLogStr);
-            }
-
-            idDnsMap.clear();
-            for (int i=0; i < mNumDnsPings; i++) {
-                for (int j = 0; j < numDnses; j++) {
-                    idDnsMap.put(mDnsPinger.pingDnsAsync(mDnsList.get(j), mDnsPingTimeoutMs,
-                            DNS_START_DELAY_MS + DNS_INTRATEST_PING_INTERVAL_MS * i), j);
-                }
-            }
-        }
-
-        @Override
-        public boolean processMessage(Message msg) {
-            if (msg.what != DnsPinger.DNS_PING_RESULT) {
-                return NOT_HANDLED;
-            }
-
-            int pingID = msg.arg1;
-            int pingResponseTime = msg.arg2;
-
-            Integer dnsServerId = idDnsMap.get(pingID);
-            if (dnsServerId == null) {
-                loge("Received a Dns response with unknown ID!");
-                return HANDLED;
-            }
-
-            idDnsMap.remove(pingID);
-            if (pingResponseTime >= 0)
-                dnsCheckSuccesses[dnsServerId]++;
-
-            if (DBG) {
-                if (pingResponseTime >= 0) {
-                    dnsResponseStrs[dnsServerId] += "|" + pingResponseTime;
-                } else {
-                    dnsResponseStrs[dnsServerId] += "|x";
-                }
-            }
-
-            /**
-             * After a full ping count, if we have more responses than this
-             * cutoff, the outcome is success; else it is 'failure'.
-             */
-
-            /**
-             * Our final success count will be at least this big, so we're
-             * guaranteed to succeed.
-             */
-            if (dnsCheckSuccesses[dnsServerId] >= mMinDnsResponses) {
-                // DNS CHECKS OK, NOW WALLED GARDEN
-                if (DBG) {
-                    log(makeLogString() + "  SUCCESS");
-                }
-
-                if (!shouldCheckWalledGarden()) {
-                    transitionTo(mOnlineWatchState);
-                    return HANDLED;
-                }
-
-                transitionTo(mDelayWalledGardenState);
-                return HANDLED;
-            }
-
-            if (idDnsMap.isEmpty()) {
-                if (DBG) {
-                    log(makeLogString() + "  FAILURE");
-                }
-                transitionTo(mDnsCheckFailureState);
-                return HANDLED;
-            }
-
-            return HANDLED;
-        }
-
-        private String makeLogString() {
-            String logStr = dnsCheckLogStr;
-            for (String respStr : dnsResponseStrs)
-                logStr += " [" + respStr + "]";
-            return logStr;
-        }
-
-        @Override
-        public void exit() {
-            mDnsPinger.cancelPings();
-        }
-
-        private boolean shouldCheckWalledGarden() {
-            if (!mWalledGardenTestEnabled) {
-                if (DBG)
-                    log("Skipping walled garden check - disabled");
-                return false;
-            }
-            long waitTime = waitTime(mWalledGardenIntervalMs,
-                    mLastWalledGardenCheckTime);
-            if (waitTime > 0) {
-                if (DBG) {
-                    log("Skipping walled garden check - wait " +
-                            waitTime + " ms.");
-                }
-                return false;
-            }
-            return true;
-        }
-    }
-
-    class DelayWalledGardenState extends State {
-        @Override
-        public void enter() {
-            sendMessageDelayed(MESSAGE_DELAYED_WALLED_GARDEN_CHECK, WALLED_GARDEN_START_DELAY_MS);
+            if (DBG) log(getName() + "\n");
+            sendMessageDelayed(obtainMessage(CMD_DELAYED_WALLED_GARDEN_CHECK,
+                    ++mWalledGardenToken, 0), WALLED_GARDEN_START_DELAY_MS);
         }
 
         @Override
         public boolean processMessage(Message msg) {
             switch (msg.what) {
-                case MESSAGE_DELAYED_WALLED_GARDEN_CHECK:
-                    mLastWalledGardenCheckTime = SystemClock.elapsedRealtime();
-                    if (isWalledGardenConnection()) {
-                        if (DBG) log("Walled garden test complete - walled garden detected");
-                        transitionTo(mWalledGardenState);
-                    } else {
-                        if (DBG) log("Walled garden test complete - online");
-                        if (mPoorNetworkDetectionEnabled) {
-                            transitionTo(mOnlineWatchState);
-                        } else {
-                            transitionTo(mOnlineState);
+                case CMD_DELAYED_WALLED_GARDEN_CHECK:
+                    if (msg.arg1 == mWalledGardenToken) {
+                        mLastWalledGardenCheckTime = SystemClock.elapsedRealtime();
+                        if (isWalledGardenConnection()) {
+                            if (DBG) log("Walled garden detected");
+                            setWalledGardenNotificationVisible(true);
                         }
+                        transitionTo(mOnlineWatchState);
                     }
-                    return HANDLED;
+                    break;
                 default:
                     return NOT_HANDLED;
             }
+            return HANDLED;
         }
     }
 
     class OnlineWatchState extends State {
-        /**
-         * Signals a short-wait message is enqueued for the current 'guard' counter
-         */
-        boolean unstableSignalChecks = false;
-
-        /**
-         * The signal is unstable.  We should enqueue a short-wait check, if one is enqueued
-         * already
-         */
-        boolean signalUnstable = false;
-
-        /**
-         * A monotonic counter to ensure that at most one check message will be processed from any
-         * set of check messages currently enqueued.  Avoids duplicate checks when a low-signal
-         * event is observed.
-         */
-        int checkGuard = 0;
-        Long lastCheckTime = null;
-
-        /** Keeps track of dns pings.  Map is from pingID to InetAddress used for ping */
-        HashMap<Integer, InetAddress> pingInfoMap = new HashMap<Integer, InetAddress>();
-
-        @Override
         public void enter() {
-            lastCheckTime = SystemClock.elapsedRealtime();
-            signalUnstable = false;
-            checkGuard++;
-            unstableSignalChecks = false;
-            pingInfoMap.clear();
-            triggerSingleDnsCheck();
+            if (DBG) log(getName() + "\n");
+            if (mPoorNetworkDetectionEnabled) {
+                //Treat entry as an rssi change
+                handleRssiChange();
+            } else {
+                transitionTo(mOnlineState);
+            }
+        }
+
+        private void handleRssiChange() {
+            if (mCurrentSignalLevel <= RSSI_LEVEL_CUTOFF) {
+                if (DBG) log("Transition out, below cut off level: " + mCurrentSignalLevel);
+                mWsmChannel.sendMessage(POOR_LINK_DETECTED);
+            } else if (mCurrentSignalLevel <= RSSI_LEVEL_MONITOR) {
+                if (DBG) log("Start monitoring, level: " + mCurrentSignalLevel);
+                sendMessage(obtainMessage(CMD_ARP_CHECK, ++mArpToken, 0));
+            }
         }
 
         @Override
         public boolean processMessage(Message msg) {
             switch (msg.what) {
                 case EVENT_RSSI_CHANGE:
-                    if (msg.arg1 != mNetEventCounter) {
-                        if (DBG) {
-                            log("Rssi change message out of sync, ignoring");
-                        }
-                        return HANDLED;
-                    }
-                    int newRssi = msg.arg2;
-                    signalUnstable = !rssiStrengthAboveCutoff(newRssi);
-                    if (DBG) {
-                        log("OnlineWatchState:: new rssi " + newRssi + " --> level " +
-                                WifiManager.calculateSignalLevel(newRssi, WIFI_SIGNAL_LEVELS));
-                    }
+                    int signalLevel = WifiManager.calculateSignalLevel(msg.arg1,
+                            WifiManager.RSSI_LEVELS);
+                    if (DBG) log("RSSI change old: " + mCurrentSignalLevel + "new: " + signalLevel);
+                    mCurrentSignalLevel = signalLevel;
 
-                    if (signalUnstable && !unstableSignalChecks) {
-                        if (DBG) {
-                            log("Sending triggered check msg");
-                        }
-                        triggerSingleDnsCheck();
-                    }
-                    return HANDLED;
-                case MESSAGE_SINGLE_DNS_CHECK:
-                    if (msg.arg1 != checkGuard) {
-                        if (DBG) {
-                            log("Single check msg out of sync, ignoring.");
-                        }
-                        return HANDLED;
-                    }
-                    lastCheckTime = SystemClock.elapsedRealtime();
-                    pingInfoMap.clear();
-                    for (InetAddress curDns: mDnsPinger.getDnsList()) {
-                        pingInfoMap.put(mDnsPinger.pingDnsAsync(curDns, mDnsPingTimeoutMs, 0),
-                                curDns);
-                    }
-                    return HANDLED;
-                case DnsPinger.DNS_PING_RESULT:
-                    InetAddress curDnsServer = pingInfoMap.get(msg.arg1);
-                    if (curDnsServer == null) {
-                        return HANDLED;
-                    }
-                    pingInfoMap.remove(msg.arg1);
-                    int responseTime = msg.arg2;
-                    if (responseTime >= 0) {
-                        if (DBG) {
-                            log("Single DNS ping OK. Response time: "
-                                    + responseTime + " from DNS " + curDnsServer);
-                        }
-                        pingInfoMap.clear();
+                    handleRssiChange();
 
-                        checkGuard++;
-                        unstableSignalChecks = false;
-                        triggerSingleDnsCheck();
-                    } else {
-                        if (pingInfoMap.isEmpty()) {
-                            if (DBG) {
-                                log("Single dns ping failure. All dns servers failed, "
-                                        + "starting full checks.");
+                    break;
+                case CMD_ARP_CHECK:
+                    if (msg.arg1 == mArpToken) {
+                        if (doArpTest(SINGLE_ARP_CHECK) != true) {
+                            if (DBG) log("single ARP fail, full ARP check");
+                            //do a full test
+                            if (doArpTest(FULL_ARP_CHECK) != true) {
+                                if (DBG) log("notify full ARP fail, level: " + mCurrentSignalLevel);
+                                mWsmChannel.sendMessage(POOR_LINK_DETECTED);
                             }
-                            transitionTo(mDnsCheckingState);
+                        }
+
+                        if (mCurrentSignalLevel <= RSSI_LEVEL_MONITOR) {
+                            if (DBG) log("Continue ARP check, rssi level: " + mCurrentSignalLevel);
+                            sendMessageDelayed(obtainMessage(CMD_ARP_CHECK, ++mArpToken, 0),
+                                    mArpCheckIntervalMs);
                         }
                     }
-                    return HANDLED;
+                    break;
+                default:
+                    return NOT_HANDLED;
             }
-            return NOT_HANDLED;
-        }
-
-        @Override
-        public void exit() {
-            mDnsPinger.cancelPings();
-        }
-
-        /**
-         * Times a dns check with an interval based on {@link #signalUnstable}
-         */
-        private void triggerSingleDnsCheck() {
-            long waitInterval;
-            if (signalUnstable) {
-                waitInterval = mDnsCheckShortIntervalMs;
-                unstableSignalChecks = true;
-            } else {
-                waitInterval = mDnsCheckLongIntervalMs;
-            }
-            sendMessageDelayed(obtainMessage(MESSAGE_SINGLE_DNS_CHECK, checkGuard, 0),
-                    waitTime(waitInterval, lastCheckTime));
+            return HANDLED;
         }
     }
 
-
     /* Child state of ConnectedState indicating that we are online
      * and there is nothing to do
      */
     class OnlineState extends State {
     }
 
-    class DnsCheckFailureState extends State {
-
-        @Override
-        public void enter() {
-            mNumCheckFailures++;
-            obtainMessage(MESSAGE_HANDLE_BAD_AP, mNetEventCounter, 0).sendToTarget();
+    private boolean shouldCheckWalledGarden() {
+        if (!mWalledGardenTestEnabled) {
+            if (DBG) log("Skipping walled garden check - disabled");
+            return false;
         }
 
-        @Override
-        public boolean processMessage(Message msg) {
-            if (msg.what != MESSAGE_HANDLE_BAD_AP) {
-                return NOT_HANDLED;
+        long waitTime = (mWalledGardenIntervalMs + mLastWalledGardenCheckTime)
+            - SystemClock.elapsedRealtime();
+
+        if (mLastWalledGardenCheckTime != 0 && waitTime > 0) {
+            if (DBG) {
+                log("Skipping walled garden check - wait " +
+                        waitTime + " ms.");
             }
+            return false;
+        }
+        return true;
+    }
 
-            if (msg.arg1 != mNetEventCounter) {
-                if (DBG) {
-                    log("Msg out of sync, ignoring...");
-                }
-                return HANDLED;
-            }
+    private boolean doArpTest(int type) {
+        boolean success;
 
-            if (mDisableAPNextFailure || mNumCheckFailures >= mBssids.size()
-                    || mNumCheckFailures >= mMaxSsidBlacklists) {
-                if (sWifiOnly) {
-                    log("Would disable bad network, but device has no mobile data!" +
-                            "  Going idle...");
-                    // This state should be called idle -- will be changing flow.
-                    transitionTo(mNotConnectedState);
-                    return HANDLED;
-                }
+        String iface = mLinkProperties.getInterfaceName();
+        String mac = mWifiInfo.getMacAddress();
+        InetAddress inetAddress = null;
+        InetAddress gateway = null;
 
-                // TODO : Unban networks if they had low signal ?
-                log("Disabling current SSID " + wifiInfoToStr(mConnectionInfo)
-                        + ".  " + "numCheckFailures " + mNumCheckFailures
-                        + ", numAPs " + mBssids.size());
-                int networkId = mConnectionInfo.getNetworkId();
-                if (!mHasConnectedWifiManager) {
-                    mWifiManager.asyncConnect(mContext, getHandler());
-                    mHasConnectedWifiManager = true;
-                }
-                mWifiManager.disableNetwork(networkId, WifiConfiguration.DISABLED_DNS_FAILURE);
-                if (mShowDisabledNotification) {
-                    setDisabledNetworkNotificationVisible(true);
-                }
-                transitionTo(mNotConnectedState);
+        for (LinkAddress la : mLinkProperties.getLinkAddresses()) {
+            inetAddress = la.getAddress();
+            break;
+        }
+
+        for (RouteInfo route : mLinkProperties.getRoutes()) {
+            gateway = route.getGateway();
+            break;
+        }
+
+        if (DBG) log("ARP " + iface + "addr: " + inetAddress + "mac: " + mac + "gw: " + gateway);
+
+        try {
+            ArpPeer peer = new ArpPeer(iface, inetAddress, mac, gateway);
+            if (type == SINGLE_ARP_CHECK) {
+                success = (peer.doArp(mArpPingTimeoutMs) != null);
+                if (DBG) log("single ARP test result: " + success);
             } else {
-                log("Blacklisting current BSSID.  " + wifiInfoToStr(mConnectionInfo)
-                       + "numCheckFailures " + mNumCheckFailures + ", numAPs " + mBssids.size());
-
-                mWifiManager.addToBlacklist(mConnectionInfo.getBSSID());
-                mWifiManager.reassociate();
-                transitionTo(mBlacklistedApState);
-            }
-            return HANDLED;
-        }
-    }
-
-    class WalledGardenState extends State {
-        @Override
-        public void enter() {
-            obtainMessage(MESSAGE_HANDLE_WALLED_GARDEN, mNetEventCounter, 0).sendToTarget();
-        }
-
-        @Override
-        public boolean processMessage(Message msg) {
-            if (msg.what != MESSAGE_HANDLE_WALLED_GARDEN) {
-                return NOT_HANDLED;
-            }
-
-            if (msg.arg1 != mNetEventCounter) {
-                if (DBG) {
-                    log("WalledGardenState::Msg out of sync, ignoring...");
+                int responses = 0;
+                for (int i=0; i < mNumArpPings; i++) {
+                    if(peer.doArp(mArpPingTimeoutMs) != null) responses++;
                 }
-                return HANDLED;
+                if (DBG) log("full ARP test result: " + responses + "/" + mNumArpPings);
+                success = (responses >= mMinArpResponses);
             }
-            setWalledGardenNotificationVisible(true);
-            if (mPoorNetworkDetectionEnabled) {
-                transitionTo(mOnlineWatchState);
-            } else {
-                transitionTo(mOnlineState);
-            }
-            return HANDLED;
+            peer.close();
+        } catch (SocketException se) {
+            //Consider an Arp socket creation issue as a successful Arp
+            //test to avoid any wifi connectivity issues
+            loge("ARP test initiation failure: " + se);
+            success = true;
         }
+
+        return success;
     }
 
-    class BlacklistedApState extends State {
-        @Override
-        public void enter() {
-            mDisableAPNextFailure = true;
-            sendMessageDelayed(obtainMessage(MESSAGE_NETWORK_FOLLOWUP, mNetEventCounter, 0),
-                    mBlacklistFollowupIntervalMs);
-        }
-
-        @Override
-        public boolean processMessage(Message msg) {
-            if (msg.what != MESSAGE_NETWORK_FOLLOWUP) {
-                return NOT_HANDLED;
-            }
-
-            if (msg.arg1 != mNetEventCounter) {
-                if (DBG) {
-                    log("BlacklistedApState::Msg out of sync, ignoring...");
-                }
-                return HANDLED;
-            }
-
-            transitionTo(mDnsCheckingState);
-            return HANDLED;
-        }
-    }
-
-
     /**
      * Convenience function for retrieving a single secure settings value
      * as a string with a default value.