/* * 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); } } } }