auto import from //depot/cupcake/@135843
diff --git a/services/java/com/android/server/WifiWatchdogService.java b/services/java/com/android/server/WifiWatchdogService.java
new file mode 100644
index 0000000..fe97b93
--- /dev/null
+++ b/services/java/com/android/server/WifiWatchdogService.java
@@ -0,0 +1,1332 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server;
+
+import android.content.BroadcastReceiver;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.database.ContentObserver;
+import android.net.NetworkInfo;
+import android.net.DhcpInfo;
+import android.net.wifi.ScanResult;
+import android.net.wifi.WifiInfo;
+import android.net.wifi.WifiManager;
+import android.net.wifi.WifiStateTracker;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.provider.Settings;
+import android.text.TextUtils;
+import android.util.Config;
+import android.util.Log;
+
+import java.io.IOException;
+import java.net.DatagramPacket;
+import java.net.DatagramSocket;
+import java.net.InetAddress;
+import java.net.SocketException;
+import java.net.SocketTimeoutException;
+import java.net.UnknownHostException;
+import java.util.List;
+import java.util.Random;
+
+/**
+ * {@link WifiWatchdogService} 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 whether the DNS server is
+ * reachable. If not, the watchdog blacklists the current access point, leading
+ * to a connection on another access point within the same network.
+ * <p>
+ * The watchdog has a few safeguards:
+ * <ul>
+ * <li>Only monitor networks with multiple access points
+ * <li>Only check at most {@link #getMaxApChecks()} different access points
+ * within the network before giving up
+ * <p>
+ * The watchdog checks for connectivity on an access point by ICMP pinging the
+ * DNS. There are settings that allow disabling the watchdog, or tweaking the
+ * acceptable packet loss (and other various parameters).
+ * <p>
+ * The core logic of the watchdog is done on the main watchdog thread. Wi-Fi
+ * callbacks can come in on other threads, so we must queue messages to the main
+ * watchdog thread's handler. Most (if not all) state is only written to from
+ * the main thread.
+ * 
+ * {@hide}
+ */
+public class WifiWatchdogService {
+    private static final String TAG = "WifiWatchdogService";
+    private static final boolean V = false || Config.LOGV;
+    private static final boolean D = true || Config.LOGD;
+    
+    /*
+     * When this was "net.dns1", sometimes the mobile data's DNS was seen
+     * instead due to a race condition. All we really care about is the
+     * DHCP-replied DNS server anyway.
+     */
+    /** The system property whose value provides the current DNS address. */
+    private static final String SYSTEMPROPERTY_KEY_DNS = "dhcp.tiwlan0.dns1";
+
+    private Context mContext;
+    private ContentResolver mContentResolver;
+    private WifiStateTracker mWifiStateTracker;
+    private WifiManager mWifiManager;
+    
+    /**
+     * The main watchdog thread.
+     */
+    private WifiWatchdogThread mThread;
+    /**
+     * The handler for the main watchdog thread.
+     */
+    private WifiWatchdogHandler mHandler;
+
+    /**
+     * The current watchdog state. Only written from the main thread!
+     */
+    private WatchdogState mState = WatchdogState.IDLE;
+    /**
+     * The SSID of the network that the watchdog is currently monitoring. Only
+     * touched in the main thread!
+     */
+    private String mSsid;
+    /**
+     * The number of access points in the current network ({@link #mSsid}) that
+     * have been checked. Only touched in the main thread!
+     */
+    private int mNumApsChecked;
+    /** Whether the current AP check should be canceled. */
+    private boolean mShouldCancel;
+    
+    WifiWatchdogService(Context context, WifiStateTracker wifiStateTracker) {
+        mContext = context;
+        mContentResolver = context.getContentResolver();
+        mWifiStateTracker = wifiStateTracker;
+        mWifiManager = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
+        
+        createThread();
+        
+        // The content observer to listen needs a handler, which createThread creates
+        registerForSettingsChanges();
+        if (isWatchdogEnabled()) {
+            registerForWifiBroadcasts();
+        }
+        
+        if (V) {
+            myLogV("WifiWatchdogService: Created");
+        }
+    }
+
+    /**
+     * Observes the watchdog on/off setting, and takes action when changed.
+     */
+    private void registerForSettingsChanges() {
+        ContentResolver contentResolver = mContext.getContentResolver();
+        contentResolver.registerContentObserver(
+                Settings.Secure.getUriFor(Settings.Secure.WIFI_WATCHDOG_ON), false,
+                new ContentObserver(mHandler) {
+            @Override
+            public void onChange(boolean selfChange) {
+                if (isWatchdogEnabled()) {
+                    registerForWifiBroadcasts();
+                } else {
+                    unregisterForWifiBroadcasts();
+                    if (mHandler != null) {
+                        mHandler.disableWatchdog();
+                    }
+                }
+            }
+        });
+    }
+
+    /**
+     * @see android.provider.Settings.Secure#WIFI_WATCHDOG_ON
+     */
+    private boolean isWatchdogEnabled() {
+        return Settings.Secure.getInt(mContentResolver, Settings.Secure.WIFI_WATCHDOG_ON, 1) == 1;
+    }
+    
+    /**
+     * @see android.provider.Settings.Secure#WIFI_WATCHDOG_AP_COUNT
+     */
+    private int getApCount() {
+        return Settings.Secure.getInt(mContentResolver,
+            Settings.Secure.WIFI_WATCHDOG_AP_COUNT, 2);
+    }
+    
+    /**
+     * @see android.provider.Settings.Secure#WIFI_WATCHDOG_INITIAL_IGNORED_PING_COUNT
+     */
+    private int getInitialIgnoredPingCount() {
+        return Settings.Secure.getInt(mContentResolver,
+            Settings.Secure.WIFI_WATCHDOG_INITIAL_IGNORED_PING_COUNT , 2);
+    }
+
+    /**
+     * @see android.provider.Settings.Secure#WIFI_WATCHDOG_PING_COUNT
+     */
+    private int getPingCount() {
+        return Settings.Secure.getInt(mContentResolver,
+            Settings.Secure.WIFI_WATCHDOG_PING_COUNT, 4);
+    }
+    
+    /**
+     * @see android.provider.Settings.Secure#WIFI_WATCHDOG_PING_TIMEOUT_MS
+     */
+    private int getPingTimeoutMs() {
+        return Settings.Secure.getInt(mContentResolver,
+            Settings.Secure.WIFI_WATCHDOG_PING_TIMEOUT_MS, 500);
+    }
+    
+    /**
+     * @see android.provider.Settings.Secure#WIFI_WATCHDOG_PING_DELAY_MS
+     */
+    private int getPingDelayMs() {
+        return Settings.Secure.getInt(mContentResolver,
+            Settings.Secure.WIFI_WATCHDOG_PING_DELAY_MS, 250);
+    }
+    
+    /**
+     * @see android.provider.Settings.Secure#WIFI_WATCHDOG_ACCEPTABLE_PACKET_LOSS_PERCENTAGE
+     */
+    private int getAcceptablePacketLossPercentage() {
+        return Settings.Secure.getInt(mContentResolver,
+            Settings.Secure.WIFI_WATCHDOG_ACCEPTABLE_PACKET_LOSS_PERCENTAGE, 25);
+    }
+    
+    /**
+     * @see android.provider.Settings.Secure#WIFI_WATCHDOG_MAX_AP_CHECKS
+     */
+    private int getMaxApChecks() {
+        return Settings.Secure.getInt(mContentResolver,
+            Settings.Secure.WIFI_WATCHDOG_MAX_AP_CHECKS, 7);
+    }
+    
+    /**
+     * @see android.provider.Settings.Secure#WIFI_WATCHDOG_BACKGROUND_CHECK_ENABLED
+     */
+    private boolean isBackgroundCheckEnabled() {
+        return Settings.Secure.getInt(mContentResolver,
+            Settings.Secure.WIFI_WATCHDOG_BACKGROUND_CHECK_ENABLED, 1) == 1;
+    }
+    
+    /**
+     * @see android.provider.Settings.Secure#WIFI_WATCHDOG_BACKGROUND_CHECK_DELAY_MS
+     */
+    private int getBackgroundCheckDelayMs() {
+        return Settings.Secure.getInt(mContentResolver,
+            Settings.Secure.WIFI_WATCHDOG_BACKGROUND_CHECK_DELAY_MS, 60000);
+    }
+    
+    /**
+     * @see android.provider.Settings.Secure#WIFI_WATCHDOG_BACKGROUND_CHECK_TIMEOUT_MS
+     */
+    private int getBackgroundCheckTimeoutMs() {
+        return Settings.Secure.getInt(mContentResolver,
+            Settings.Secure.WIFI_WATCHDOG_BACKGROUND_CHECK_TIMEOUT_MS, 1000);
+    }
+
+    /**
+     * @see android.provider.Settings.Secure#WIFI_WATCHDOG_WATCH_LIST
+     * @return the comma-separated list of SSIDs
+     */
+    private String getWatchList() {
+        return Settings.Secure.getString(mContentResolver,
+                Settings.Secure.WIFI_WATCHDOG_WATCH_LIST);
+    }
+    
+    /**
+     * Registers to receive the necessary Wi-Fi broadcasts.
+     */
+    private void registerForWifiBroadcasts() {
+        IntentFilter intentFilter = new IntentFilter();
+        intentFilter.addAction(WifiManager.NETWORK_STATE_CHANGED_ACTION);
+        intentFilter.addAction(WifiManager.SUPPLICANT_CONNECTION_CHANGE_ACTION);
+        intentFilter.addAction(WifiManager.WIFI_STATE_CHANGED_ACTION);
+        mContext.registerReceiver(mReceiver, intentFilter);
+    }
+
+    /**
+     * Unregisters from receiving the Wi-Fi broadcasts.
+     */
+    private void unregisterForWifiBroadcasts() {
+        mContext.unregisterReceiver(mReceiver);
+    }
+
+    /**
+     * Creates the main watchdog thread, including waiting for the handler to be
+     * created.
+     */
+    private void createThread() {
+        mThread = new WifiWatchdogThread();
+        mThread.start();
+        waitForHandlerCreation();
+    }
+
+    /**
+     * Waits for the main watchdog thread to create the handler.
+     */
+    private void waitForHandlerCreation() {
+        synchronized(this) {
+            while (mHandler == null) {
+                try {
+                    // Wait for the handler to be set by the other thread
+                    wait();
+                } catch (InterruptedException e) {
+                    Log.e(TAG, "Interrupted while waiting on handler.");
+                }
+            }
+        }
+    }
+
+    // Utility methods
+    
+    /**
+     * Logs with the current thread.
+     */
+    private static void myLogV(String message) {
+        Log.v(TAG, "(" + Thread.currentThread().getName() + ") " + message);
+    }
+    
+    private static void myLogD(String message) {
+        Log.d(TAG, "(" + Thread.currentThread().getName() + ") " + message);
+    }
+    
+    /**
+     * Gets the DNS of the current AP.
+     * 
+     * @return The DNS of the current AP.
+     */
+    private int getDns() {
+        DhcpInfo addressInfo = mWifiManager.getDhcpInfo();
+        if (addressInfo != null) {
+            return addressInfo.dns1;
+        } else {
+            return -1;
+        }
+    }
+    
+    /**
+     * Checks whether the DNS can be reached using multiple attempts according
+     * to the current setting values.
+     * 
+     * @return Whether the DNS is reachable
+     */
+    private boolean checkDnsConnectivity() {
+        int dns = getDns();
+        if (dns == -1) {
+            if (V) {
+                myLogV("checkDnsConnectivity: Invalid DNS, returning false");
+            }
+            return false;
+        }
+        
+        if (V) {
+            myLogV("checkDnsConnectivity: Checking 0x" +
+                    Integer.toHexString(Integer.reverseBytes(dns)) + " for connectivity");
+        }
+
+        int numInitialIgnoredPings = getInitialIgnoredPingCount();
+        int numPings = getPingCount();
+        int pingDelay = getPingDelayMs();
+        int acceptableLoss = getAcceptablePacketLossPercentage();
+        
+        /** See {@link Secure#WIFI_WATCHDOG_INITIAL_IGNORED_PING_COUNT} */
+        int ignoredPingCounter = 0;
+        int pingCounter = 0;
+        int successCounter = 0;
+        
+        // No connectivity check needed
+        if (numPings == 0) {
+            return true;
+        }
+
+        // Do the initial pings that we ignore
+        for (; ignoredPingCounter < numInitialIgnoredPings; ignoredPingCounter++) {
+            if (shouldCancel()) return false;
+
+            boolean dnsAlive = DnsPinger.isDnsReachable(dns, getPingTimeoutMs());
+            if (dnsAlive) {
+                /*
+                 * Successful "ignored" pings are *not* ignored (they count in the total number
+                 * of pings), but failures are really ignored.
+                 */
+                pingCounter++;
+                successCounter++;
+            }
+            
+            if (V) {
+                Log.v(TAG, (dnsAlive ? "  +" : "  Ignored: -"));
+            }
+
+            if (shouldCancel()) return false;
+            
+            try {
+                Thread.sleep(pingDelay);
+            } catch (InterruptedException e) {
+                Log.w(TAG, "Interrupted while pausing between pings", e);
+            }
+        }
+        
+        // Do the pings that we use to measure packet loss
+        for (; pingCounter < numPings; pingCounter++) {
+            if (shouldCancel()) return false;
+
+            if (DnsPinger.isDnsReachable(dns, getPingTimeoutMs())) {
+                successCounter++;
+                if (V) {
+                    Log.v(TAG, "  +");
+                }
+            } else {
+                if (V) {
+                    Log.v(TAG, "  -");
+                }
+            }
+
+            if (shouldCancel()) return false;
+            
+            try {
+                Thread.sleep(pingDelay);
+            } catch (InterruptedException e) {
+                Log.w(TAG, "Interrupted while pausing between pings", e);
+            }
+        }
+        
+        int packetLossPercentage = 100 * (numPings - successCounter) / numPings;
+        if (D) {
+            Log.d(TAG, packetLossPercentage
+                    + "% packet loss (acceptable is " + acceptableLoss + "%)");
+        }
+        
+        return !shouldCancel() && (packetLossPercentage <= acceptableLoss);
+    }
+
+    private boolean backgroundCheckDnsConnectivity() {
+        int dns = getDns();
+        if (false && V) {
+            myLogV("backgroundCheckDnsConnectivity: Background checking " + dns +
+                    " for connectivity");
+        }
+        
+        if (dns == -1) {
+            if (V) {
+                myLogV("backgroundCheckDnsConnectivity: DNS is empty, returning false");
+            }
+            return false;
+        }
+        
+        return DnsPinger.isDnsReachable(dns, getBackgroundCheckTimeoutMs());
+    }
+    
+    /**
+     * Signals the current action to cancel.
+     */
+    private void cancelCurrentAction() {
+        mShouldCancel = true;
+    }
+    
+    /**
+     * Helper to check whether to cancel. 
+     * 
+     * @return Whether to cancel processing the action.
+     */
+    private boolean shouldCancel() {
+        if (V && mShouldCancel) {
+            myLogV("shouldCancel: Cancelling");
+        }
+        
+        return mShouldCancel;
+    }
+    
+    // Wi-Fi initiated callbacks (could be executed in another thread)
+
+    /**
+     * Called when connected to an AP (this can be the next AP in line, or
+     * it can be a completely different network).
+     * 
+     * @param ssid The SSID of the access point.
+     * @param bssid The BSSID of the access point.
+     */
+    private void onConnected(String ssid, String bssid) {
+        if (V) {
+            myLogV("onConnected: SSID: " + ssid + ", BSSID: " + bssid);
+        }
+
+        /*
+         * The current action being processed by the main watchdog thread is now
+         * stale, so cancel it.
+         */
+        cancelCurrentAction();
+        
+        if ((mSsid == null) || !mSsid.equals(ssid)) {
+            /*
+             * This is a different network than what the main watchdog thread is
+             * processing, dispatch the network change message on the main thread.
+             */
+            mHandler.dispatchNetworkChanged(ssid);
+        }
+        
+        if (requiresWatchdog(ssid, bssid)) {
+            if (D) {
+                myLogD(ssid + " (" + bssid + ") requires the watchdog");
+            }
+
+            // This access point requires a watchdog, so queue the check on the main thread
+            mHandler.checkAp(new AccessPoint(ssid, bssid));
+            
+        } else {
+            if (D) {
+                myLogD(ssid + " (" + bssid + ") does not require the watchdog");
+            }
+
+            // This access point does not require a watchdog, so queue idle on the main thread
+            mHandler.idle();
+        }
+    }
+    
+    /**
+     * Called when Wi-Fi is enabled.
+     */
+    private void onEnabled() {
+        cancelCurrentAction();
+        // Queue a hard-reset of the state on the main thread
+        mHandler.reset();
+    }
+    
+    /**
+     * Called when disconnected (or some other event similar to being disconnected).
+     */
+    private void onDisconnected() {
+        if (V) {
+            myLogV("onDisconnected");
+        }
+        
+        /*
+         * Disconnected from an access point, the action being processed by the
+         * watchdog thread is now stale, so cancel it.
+         */
+        cancelCurrentAction();
+        // Dispatch the disconnected to the main watchdog thread
+        mHandler.dispatchDisconnected();
+        // Queue the action to go idle
+        mHandler.idle();
+    }
+
+    /**
+     * Checks whether an access point requires watchdog monitoring.
+     * 
+     * @param ssid The SSID of the access point.
+     * @param bssid The BSSID of the access point.
+     * @return Whether the access point/network should be monitored by the
+     *         watchdog.
+     */
+    private boolean requiresWatchdog(String ssid, String bssid) {
+        if (V) {
+            myLogV("requiresWatchdog: SSID: " + ssid + ", BSSID: " + bssid);
+        }
+        
+        WifiInfo info = null;
+        if (ssid == null) {
+            /*
+             * This is called from a Wi-Fi callback, so assume the WifiInfo does
+             * not have stale data.
+             */
+            info = mWifiManager.getConnectionInfo();
+            ssid = info.getSSID();
+            if (ssid == null) {
+                // It's still null, give up
+                if (V) {
+                    Log.v(TAG, "  Invalid SSID, returning false");
+                }
+                return false;
+            }
+        }
+        
+        if (TextUtils.isEmpty(bssid)) {
+            // Similar as above
+            if (info == null) {
+                info = mWifiManager.getConnectionInfo();
+            }
+            bssid = info.getBSSID();
+            if (TextUtils.isEmpty(bssid)) {
+                // It's still null, give up
+                if (V) {
+                    Log.v(TAG, "  Invalid BSSID, returning false");
+                }
+                return false;
+            }
+        }
+
+        if (!isOnWatchList(ssid)) {
+            if (V) {
+                Log.v(TAG, "  SSID not on watch list, returning false");
+            }
+            return false;
+        }
+
+        // The watchdog only monitors networks with multiple APs
+        if (!hasRequiredNumberOfAps(ssid)) {
+            return false;
+        }
+
+        return true;
+    }
+
+    private boolean isOnWatchList(String ssid) {
+        String watchList;
+
+        if (ssid == null || (watchList = getWatchList()) == null) {
+            return false;
+        }
+
+        String[] list = watchList.split(" *, *");
+
+        for (String name : list) {
+            if (ssid.equals(name)) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+    
+    /**
+     * Checks if the current scan results have multiple access points with an SSID.
+     * 
+     * @param ssid The SSID to check.
+     * @return Whether the SSID has multiple access points.
+     */
+    private boolean hasRequiredNumberOfAps(String ssid) {
+        List<ScanResult> results = mWifiManager.getScanResults();
+        if (results == null) {
+            if (V) {
+                myLogV("hasRequiredNumberOfAps: Got null scan results, returning false");
+            }
+            return false;
+        }
+        
+        int numApsRequired = getApCount();
+        int numApsFound = 0;
+        int resultsSize = results.size();
+        for (int i = 0; i < resultsSize; i++) {
+            ScanResult result = results.get(i);
+            if (result == null) continue;
+            if (result.SSID == null) continue;
+            
+            if (result.SSID.equals(ssid)) {
+                numApsFound++;
+                
+                if (numApsFound >= numApsRequired) {
+                    if (V) {
+                        myLogV("hasRequiredNumberOfAps: SSID: " + ssid + ", returning true");
+                    }
+                    return true;
+                }
+            }
+        }
+        
+        if (V) {
+            myLogV("hasRequiredNumberOfAps: SSID: " + ssid + ", returning false");
+        }
+        return false;
+    }
+    
+    // Watchdog logic (assume all of these methods will be in our main thread)
+    
+    /**
+     * Handles a Wi-Fi network change (for example, from networkA to networkB).
+     */
+    private void handleNetworkChanged(String ssid) {
+        // Set the SSID being monitored to the new SSID 
+        mSsid = ssid;
+        // Set various state to that when being idle 
+        setIdleState(true);
+    }
+    
+    /**
+     * Handles checking whether an AP is a "good" AP.  If not, it will be blacklisted.
+     * 
+     * @param ap The access point to check.
+     */
+    private void handleCheckAp(AccessPoint ap) {
+        // Reset the cancel state since this is the entry point of this action
+        mShouldCancel = false;
+        
+        if (V) {
+            myLogV("handleCheckAp: AccessPoint: " + ap);
+        }
+        
+        // Make sure we are not sleeping
+        if (mState == WatchdogState.SLEEP) {
+            if (V) {
+                Log.v(TAG, "  Sleeping (in " + mSsid + "), so returning");
+            }
+            return;
+        }
+        
+        mState = WatchdogState.CHECKING_AP;
+        
+        /*
+         * Checks to make sure we haven't exceeded the max number of checks
+         * we're allowed per network
+         */
+        mNumApsChecked++;
+        if (mNumApsChecked > getMaxApChecks()) {
+            if (V) {
+                Log.v(TAG, "  Passed the max attempts (" + getMaxApChecks()
+                        + "), going to sleep for " + mSsid);
+            }
+            mHandler.sleep(mSsid);
+            return;
+        }
+
+        // Do the check
+        boolean isApAlive = checkDnsConnectivity();
+        
+        if (V) {
+            Log.v(TAG, "  Is it alive: " + isApAlive);
+        }
+
+        // Take action based on results
+        if (isApAlive) {
+            handleApAlive(ap);
+        } else {
+            handleApUnresponsive(ap);
+        }
+    }
+
+    /**
+     * Handles the case when an access point is alive.
+     * 
+     * @param ap The access point.
+     */
+    private void handleApAlive(AccessPoint ap) {
+        // Check whether we are stale and should cancel
+        if (shouldCancel()) return;
+        // We're satisfied with this AP, so go idle
+        setIdleState(false);
+        
+        if (D) {
+            myLogD("AP is alive: " + ap.toString());
+        }
+        
+        // Queue the next action to be a background check
+        mHandler.backgroundCheckAp(ap);
+    }
+    
+    /**
+     * Handles an unresponsive AP by blacklisting it.
+     * 
+     * @param ap The access point.
+     */
+    private void handleApUnresponsive(AccessPoint ap) {
+        // Check whether we are stale and should cancel
+        if (shouldCancel()) return;
+        // This AP is "bad", switch to another
+        mState = WatchdogState.SWITCHING_AP;
+
+        if (D) {
+            myLogD("AP is dead: " + ap.toString());
+        }
+        
+        // Black list this "bad" AP, this will cause an attempt to connect to another
+        blacklistAp(ap.bssid);
+    }
+
+    private void blacklistAp(String bssid) {
+        if (TextUtils.isEmpty(bssid)) {
+            return;
+        }
+        
+        // Before taking action, make sure we should not cancel our processing
+        if (shouldCancel()) return;
+        
+        if (!mWifiStateTracker.addToBlacklist(bssid)) {
+            // There's a known bug where this method returns failure on success
+            //Log.e(TAG, "Blacklisting " + bssid + " failed");
+        }
+
+        if (D) {
+            myLogD("Blacklisting " + bssid);
+        }
+    }
+
+    /**
+     * Handles a single background check. If it fails, it should trigger a
+     * normal check. If it succeeds, it should queue another background check.
+     * 
+     * @param ap The access point to do a background check for. If this is no
+     *        longer the current AP, it is okay to return without any
+     *        processing.
+     */
+    private void handleBackgroundCheckAp(AccessPoint ap) {
+        // Reset the cancel state since this is the entry point of this action
+        mShouldCancel = false;
+        
+        if (false && V) {
+            myLogV("handleBackgroundCheckAp: AccessPoint: " + ap);
+        }
+        
+        // Make sure we are not sleeping
+        if (mState == WatchdogState.SLEEP) {
+            if (V) {
+                Log.v(TAG, "  handleBackgroundCheckAp: Sleeping (in " + mSsid + "), so returning");
+            }
+            return;
+        }
+        
+        // Make sure the AP we're supposed to be background checking is still the active one
+        WifiInfo info = mWifiManager.getConnectionInfo();
+        if (info.getSSID() == null || !info.getSSID().equals(ap.ssid)) {
+            if (V) {
+                myLogV("handleBackgroundCheckAp: We are no longer connected to "
+                        + ap + ", and instead are on " + info);
+            }
+            return;
+        }
+        
+        if (info.getBSSID() == null || !info.getBSSID().equals(ap.bssid)) {
+            if (V) {
+                myLogV("handleBackgroundCheckAp: We are no longer connected to "
+                        + ap + ", and instead are on " + info);
+            }
+            return;
+        }
+
+        // Do the check
+        boolean isApAlive = backgroundCheckDnsConnectivity();
+        
+        if (V && !isApAlive) {
+            Log.v(TAG, "  handleBackgroundCheckAp: Is it alive: " + isApAlive);
+        }
+
+        if (shouldCancel()) {
+            return;
+        }
+        
+        // Take action based on results
+        if (isApAlive) {
+            // Queue another background check
+            mHandler.backgroundCheckAp(ap);
+            
+        } else {
+            if (D) {
+                myLogD("Background check failed for " + ap.toString());
+            }
+            
+            // Queue a normal check, so it can take proper action
+            mHandler.checkAp(ap);
+        }
+    }
+    
+    /**
+     * Handles going to sleep for this network. Going to sleep means we will not
+     * monitor this network anymore.
+     * 
+     * @param ssid The network that will not be monitored anymore.
+     */
+    private void handleSleep(String ssid) {
+        // Make sure the network we're trying to sleep in is still the current network
+        if (ssid != null && ssid.equals(mSsid)) {
+            mState = WatchdogState.SLEEP;
+
+            if (D) {
+                myLogD("Going to sleep for " + ssid);
+            }
+            
+            /*
+             * Before deciding to go to sleep, we may have checked a few APs
+             * (and blacklisted them). Clear the blacklist so the AP with best
+             * signal is chosen.
+             */
+            if (!mWifiStateTracker.clearBlacklist()) {
+                // There's a known bug where this method returns failure on success
+                //Log.e(TAG, "Clearing blacklist failed");
+            }
+            
+            if (V) {
+                myLogV("handleSleep: Set state to SLEEP and cleared blacklist");
+            }
+        }
+    }
+
+    /**
+     * Handles an access point disconnection.
+     */
+    private void handleDisconnected() {
+        /*
+         * We purposefully do not change mSsid to null. This is to handle
+         * disconnected followed by connected better (even if there is some
+         * duration in between). For example, if the watchdog went to sleep in a
+         * network, and then the phone goes to sleep, when the phone wakes up we
+         * still want to be in the sleeping state. When the phone went to sleep,
+         * we would have gotten a disconnected event which would then set mSsid
+         * = null. This is bad, since the following connect would cause us to do
+         * the "network is good?" check all over again. */
+        
+        /* 
+         * Set the state as if we were idle (don't come out of sleep, only
+         * hard reset and network changed should do that.
+         */
+        setIdleState(false);
+    }
+
+    /**
+     * Handles going idle. Idle means we are satisfied with the current state of
+     * things, but if a new connection occurs we'll re-evaluate.
+     */
+    private void handleIdle() {
+        // Reset the cancel state since this is the entry point for this action
+        mShouldCancel = false;
+        
+        if (V) {
+            myLogV("handleSwitchToIdle");
+        }
+        
+        // If we're sleeping, don't do anything
+        if (mState == WatchdogState.SLEEP) {
+            Log.v(TAG, "  Sleeping (in " + mSsid + "), so returning");
+            return;
+        }
+        
+        // Set the idle state
+        setIdleState(false);
+        
+        if (V) {
+            Log.v(TAG, "  Set state to IDLE");
+        }
+    }
+    
+    /**
+     * Sets the state as if we are going idle.
+     */
+    private void setIdleState(boolean forceIdleState) {
+        // Setting idle state does not kick us out of sleep unless the forceIdleState is set
+        if (forceIdleState || (mState != WatchdogState.SLEEP)) {
+            mState = WatchdogState.IDLE;
+        }
+        mNumApsChecked = 0;
+    }
+
+    /**
+     * Handles a hard reset. A hard reset is rarely used, but when used it
+     * should revert anything done by the watchdog monitoring.
+     */
+    private void handleReset() {
+        mWifiStateTracker.clearBlacklist();
+        setIdleState(true);
+    }
+    
+    // Inner classes
+
+    /**
+     * Possible states for the watchdog to be in.
+     */
+    private static enum WatchdogState {
+        /** The watchdog is currently idle, but it is still responsive to future AP checks in this network. */
+        IDLE,
+        /** The watchdog is sleeping, so it will not try any AP checks for the network. */
+        SLEEP,
+        /** The watchdog is currently checking an AP for connectivity. */
+        CHECKING_AP,
+        /** The watchdog is switching to another AP in the network. */
+        SWITCHING_AP
+    }
+
+    /**
+     * The main thread for the watchdog monitoring. This will be turned into a
+     * {@link Looper} thread.
+     */
+    private class WifiWatchdogThread extends Thread {
+        WifiWatchdogThread() {
+            super("WifiWatchdogThread");
+        }
+        
+        @Override
+        public void run() {
+            // Set this thread up so the handler will work on it
+            Looper.prepare();
+            
+            synchronized(WifiWatchdogService.this) {
+                mHandler = new WifiWatchdogHandler();
+
+                // Notify that the handler has been created
+                WifiWatchdogService.this.notify();
+            }
+            
+            // Listen for messages to the handler
+            Looper.loop();
+        }
+    }
+
+    /**
+     * The main thread's handler. There are 'actions', and just general 
+     * 'messages'. There should only ever be one 'action' in the queue (aside
+     * from the one being processed, if any). There may be multiple messages in
+     * the queue. So, actions are replaced by more recent actions, where as
+     * messages will be executed for sure. Messages end up being used to just
+     * change some state, and not really take any action.
+     * <p>
+     * There is little logic inside this class, instead methods of the form
+     * "handle___" are called in the main {@link WifiWatchdogService}.
+     */
+    private class WifiWatchdogHandler extends Handler {
+        /** Check whether the AP is "good".  The object will be an {@link AccessPoint}. */
+        static final int ACTION_CHECK_AP = 1;
+        /** Go into the idle state. */
+        static final int ACTION_IDLE = 2;
+        /**
+         * Performs a periodic background check whether the AP is still "good".
+         * The object will be an {@link AccessPoint}.
+         */
+        static final int ACTION_BACKGROUND_CHECK_AP = 3;
+
+        /**
+         * Go to sleep for the current network. We are conservative with making
+         * this a message rather than action. We want to make sure our main
+         * thread sees this message, but if it were an action it could be
+         * removed from the queue and replaced by another action. The main
+         * thread will ensure when it sees the message that the state is still
+         * valid for going to sleep.
+         * <p>
+         * For an explanation of sleep, see {@link android.provider.Settings.Secure#WIFI_WATCHDOG_MAX_AP_CHECKS}.
+         */
+        static final int MESSAGE_SLEEP = 101;
+        /** Disables the watchdog. */
+        static final int MESSAGE_DISABLE_WATCHDOG = 102;
+        /** The network has changed. */
+        static final int MESSAGE_NETWORK_CHANGED = 103;
+        /** The current access point has disconnected. */
+        static final int MESSAGE_DISCONNECTED = 104;
+        /** Performs a hard-reset on the watchdog state. */
+        static final int MESSAGE_RESET = 105;
+        
+        void checkAp(AccessPoint ap) {
+            removeAllActions();
+            sendMessage(obtainMessage(ACTION_CHECK_AP, ap));
+        }
+        
+        void backgroundCheckAp(AccessPoint ap) {
+            if (!isBackgroundCheckEnabled()) return;
+            
+            removeAllActions();
+            sendMessageDelayed(obtainMessage(ACTION_BACKGROUND_CHECK_AP, ap),
+                    getBackgroundCheckDelayMs());
+        }
+        
+        void idle() {
+            removeAllActions();
+            sendMessage(obtainMessage(ACTION_IDLE));
+        }
+        
+        void sleep(String ssid) {
+            removeAllActions();
+            sendMessage(obtainMessage(MESSAGE_SLEEP, ssid));
+        }
+        
+        void disableWatchdog() {
+            removeAllActions();
+            sendMessage(obtainMessage(MESSAGE_DISABLE_WATCHDOG));
+        }
+        
+        void dispatchNetworkChanged(String ssid) {
+            removeAllActions();
+            sendMessage(obtainMessage(MESSAGE_NETWORK_CHANGED, ssid));
+        }
+
+        void dispatchDisconnected() {
+            removeAllActions();
+            sendMessage(obtainMessage(MESSAGE_DISCONNECTED));
+        }
+
+        void reset() {
+            removeAllActions();
+            sendMessage(obtainMessage(MESSAGE_RESET));
+        }
+        
+        private void removeAllActions() {
+            removeMessages(ACTION_CHECK_AP);
+            removeMessages(ACTION_IDLE);
+            removeMessages(ACTION_BACKGROUND_CHECK_AP);
+        }
+        
+        @Override
+        public void handleMessage(Message msg) {
+            switch (msg.what) {
+                case MESSAGE_NETWORK_CHANGED:
+                    handleNetworkChanged((String) msg.obj);
+                    break;
+                case ACTION_CHECK_AP:
+                    handleCheckAp((AccessPoint) msg.obj);
+                    break;
+                case ACTION_BACKGROUND_CHECK_AP:
+                    handleBackgroundCheckAp((AccessPoint) msg.obj);
+                    break;
+                case MESSAGE_SLEEP:
+                    handleSleep((String) msg.obj);
+                    break;
+                case ACTION_IDLE:
+                    handleIdle();
+                    break;
+                case MESSAGE_DISABLE_WATCHDOG:
+                    handleIdle();
+                    break;
+                case MESSAGE_DISCONNECTED:
+                    handleDisconnected();
+                    break;
+                case MESSAGE_RESET:
+                    handleReset();
+                    break;
+            }
+        }
+    }
+
+    /**
+     * Receives Wi-Fi broadcasts.
+     * <p>
+     * There is little logic in this class, instead methods of the form "on___"
+     * are called in the {@link WifiWatchdogService}.
+     */
+    private BroadcastReceiver mReceiver = new BroadcastReceiver() {
+
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            final String action = intent.getAction();
+            if (action.equals(WifiManager.NETWORK_STATE_CHANGED_ACTION)) {
+                handleNetworkStateChanged(
+                        (NetworkInfo) intent.getParcelableExtra(WifiManager.EXTRA_NETWORK_INFO));
+            } else if (action.equals(WifiManager.SUPPLICANT_CONNECTION_CHANGE_ACTION)) {
+                handleSupplicantConnectionChanged(
+                        intent.getBooleanExtra(WifiManager.EXTRA_SUPPLICANT_CONNECTED, false));
+            } else if (action.equals(WifiManager.WIFI_STATE_CHANGED_ACTION)) {
+                handleWifiStateChanged(intent.getIntExtra(WifiManager.EXTRA_WIFI_STATE,
+                        WifiManager.WIFI_STATE_UNKNOWN));
+            }
+        }
+
+        private void handleNetworkStateChanged(NetworkInfo info) {
+            if (V) {
+                myLogV("Receiver.handleNetworkStateChanged: NetworkInfo: "
+                        + info);
+            }
+            
+            switch (info.getState()) {
+                case CONNECTED:
+                    WifiInfo wifiInfo = mWifiManager.getConnectionInfo();
+                    if (wifiInfo.getSSID() == null || wifiInfo.getBSSID() == null) {
+                        if (V) {
+                            myLogV("handleNetworkStateChanged: Got connected event but SSID or BSSID are null. SSID: "
+                                + wifiInfo.getSSID()
+                                + ", BSSID: "
+                                + wifiInfo.getBSSID() + ", ignoring event");
+                        }
+                        return;
+                    }
+                    onConnected(wifiInfo.getSSID(), wifiInfo.getBSSID());
+                    break;
+
+                case DISCONNECTED:
+                    onDisconnected();
+                    break;
+            }
+        }
+
+        private void handleSupplicantConnectionChanged(boolean connected) {
+            if (!connected) {
+                onDisconnected();
+            }
+        }
+        
+        private void handleWifiStateChanged(int wifiState) {
+            if (wifiState == WifiManager.WIFI_STATE_DISABLED) {
+                onDisconnected();
+            } else if (wifiState == WifiManager.WIFI_STATE_ENABLED) {
+                onEnabled();
+            }
+        }
+    };
+
+    /**
+     * Describes an access point by its SSID and BSSID.
+     */
+    private static class AccessPoint {
+        String ssid;
+        String bssid;
+        
+        AccessPoint(String ssid, String bssid) {
+            this.ssid = ssid;
+            this.bssid = bssid;
+        }
+
+        private boolean hasNull() {
+            return ssid == null || bssid == null;
+        }
+        
+        @Override
+        public boolean equals(Object o) {
+            if (!(o instanceof AccessPoint)) return false;
+            AccessPoint otherAp = (AccessPoint) o;
+            boolean iHaveNull = hasNull();
+            // Either we both have a null, or our SSIDs and BSSIDs are equal
+            return (iHaveNull && otherAp.hasNull()) || 
+                    (otherAp.bssid != null && ssid.equals(otherAp.ssid)
+                    && bssid.equals(otherAp.bssid));
+        }
+        
+        @Override
+        public int hashCode() {
+            if (ssid == null || bssid == null) return 0;
+            return ssid.hashCode() + bssid.hashCode();
+        }
+
+        @Override
+        public String toString() {
+            return ssid + " (" + bssid + ")";
+        }
+    }
+
+    /**
+     * Performs a simple DNS "ping" by sending a "server status" query packet to
+     * the DNS server. As long as the server replies, we consider it a success.
+     * <p>
+     * We do not use a simple hostname lookup because that could be cached and
+     * the API may not differentiate between a time out and a failure lookup
+     * (which we really care about).
+     */
+    private static class DnsPinger {
+        
+        /** Number of bytes for the query */
+        private static final int DNS_QUERY_BASE_SIZE = 33;
+        
+        /** The DNS port */
+        private static final int DNS_PORT = 53;
+        
+        /** Used to generate IDs */
+        private static Random sRandom = new Random();
+        
+        static boolean isDnsReachable(int dns, int timeout) {
+            try {
+                DatagramSocket socket = new DatagramSocket();
+                
+                // Set some socket properties
+                socket.setSoTimeout(timeout);
+                
+                byte[] buf = new byte[DNS_QUERY_BASE_SIZE];
+                fillQuery(buf);
+                
+                // Send the DNS query
+                byte parts[] = new byte[4];
+                parts[0] = (byte)(dns & 0xff);
+                parts[1] = (byte)((dns >> 8) & 0xff);
+                parts[2] = (byte)((dns >> 16) & 0xff);
+                parts[3] = (byte)((dns >> 24) & 0xff);
+
+                InetAddress dnsAddress = InetAddress.getByAddress(parts);
+                DatagramPacket packet = new DatagramPacket(buf,
+                        buf.length, dnsAddress, DNS_PORT);
+                socket.send(packet);
+                
+                // Wait for reply (blocks for the above timeout)
+                DatagramPacket replyPacket = new DatagramPacket(buf, buf.length);
+                socket.receive(replyPacket);
+
+                // If a timeout occurred, an exception would have been thrown.  We got a reply!
+                return true;
+                
+            } catch (SocketException e) {
+                if (V) {
+                    Log.v(TAG, "DnsPinger.isReachable received SocketException", e);
+                }
+                return false;
+                
+            } catch (UnknownHostException e) {
+                if (V) {
+                    Log.v(TAG, "DnsPinger.isReachable is unable to resolve the DNS host", e);
+                }
+                return false;
+
+            } catch (SocketTimeoutException e) {
+                return false;
+                
+            } catch (IOException e) {
+                if (V) {
+                    Log.v(TAG, "DnsPinger.isReachable got an IOException", e);
+                }
+                return false;
+                
+            } catch (Exception e) {
+                if (V || Config.LOGD) {
+                    Log.d(TAG, "DnsPinger.isReachable got an unknown exception", e);
+                }
+                return false;
+            }
+        }
+        
+        private static void fillQuery(byte[] buf) {
+
+            /*
+             * See RFC2929 (though the bit tables in there are misleading for
+             * us. For example, the recursion desired bit is the 0th bit for us,
+             * but looking there it would appear as the 7th bit of the byte
+             */
+
+            // Make sure it's all zeroed out
+            for (int i = 0; i < buf.length; i++) buf[i] = 0;
+
+            // Form a query for www.android.com
+            
+            // [0-1] bytes are an ID, generate random ID for this query
+            buf[0] = (byte) sRandom.nextInt(256); 
+            buf[1] = (byte) sRandom.nextInt(256); 
+            
+            // [2-3] bytes are for flags.
+            buf[2] = 1; // Recursion desired
+
+            // [4-5] bytes are for the query count
+            buf[5] = 1; // One query 
+            
+            // [6-7] [8-9] [10-11] are all counts of other fields we don't use
+
+            // [12-15] for www
+            writeString(buf, 12, "www");
+            
+            // [16-23] for android
+            writeString(buf, 16, "android");
+            
+            // [24-27] for com
+            writeString(buf, 24, "com");
+            
+            // [29-30] bytes are for QTYPE, set to 1 
+            buf[30] = 1;
+
+            // [31-32] bytes are for QCLASS, set to 1 
+            buf[32] = 1;
+        }
+        
+        private static void writeString(byte[] buf, int startPos, String string) {
+            int pos = startPos;
+            
+            // Write the length first
+            buf[pos++] = (byte) string.length();
+            for (int i = 0; i < string.length(); i++) {
+                buf[pos++] = (byte) string.charAt(i);
+            }
+        }
+    }
+}