| /* |
| * 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.Slog; |
| |
| 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; |
| |
| 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; |
| |
| private ContentObserver mContentObserver; |
| |
| /** |
| * 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, |
| mContentObserver = 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.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(); |
| } |
| |
| /** |
| * Unregister broadcasts and quit the watchdog thread |
| */ |
| private void quit() { |
| unregisterForWifiBroadcasts(); |
| mContext.getContentResolver().unregisterContentObserver(mContentObserver); |
| mHandler.removeAllActions(); |
| mHandler.getLooper().quit(); |
| } |
| |
| /** |
| * 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) { |
| Slog.e(TAG, "Interrupted while waiting on handler."); |
| } |
| } |
| } |
| } |
| |
| // Utility methods |
| |
| /** |
| * Logs with the current thread. |
| */ |
| private static void myLogV(String message) { |
| Slog.v(TAG, "(" + Thread.currentThread().getName() + ") " + message); |
| } |
| |
| private static void myLogD(String message) { |
| Slog.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) { |
| Slog.v(TAG, (dnsAlive ? " +" : " Ignored: -")); |
| } |
| |
| if (shouldCancel()) return false; |
| |
| try { |
| Thread.sleep(pingDelay); |
| } catch (InterruptedException e) { |
| Slog.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) { |
| Slog.v(TAG, " +"); |
| } |
| } else { |
| if (V) { |
| Slog.v(TAG, " -"); |
| } |
| } |
| |
| if (shouldCancel()) return false; |
| |
| try { |
| Thread.sleep(pingDelay); |
| } catch (InterruptedException e) { |
| Slog.w(TAG, "Interrupted while pausing between pings", e); |
| } |
| } |
| |
| int packetLossPercentage = 100 * (numPings - successCounter) / numPings; |
| if (D) { |
| Slog.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) { |
| Slog.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) { |
| Slog.v(TAG, " Invalid BSSID, returning false"); |
| } |
| return false; |
| } |
| } |
| |
| if (!isOnWatchList(ssid)) { |
| if (V) { |
| Slog.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) { |
| Slog.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) { |
| Slog.v(TAG, " Passed the max attempts (" + getMaxApChecks() |
| + "), going to sleep for " + mSsid); |
| } |
| mHandler.sleep(mSsid); |
| return; |
| } |
| |
| // Do the check |
| boolean isApAlive = checkDnsConnectivity(); |
| |
| if (V) { |
| Slog.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); |
| // Initiate an association to an alternate AP |
| mWifiStateTracker.reassociate(); |
| } |
| |
| 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 |
| //Slog.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) { |
| Slog.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) { |
| Slog.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 |
| //Slog.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) { |
| Slog.v(TAG, " Sleeping (in " + mSsid + "), so returning"); |
| return; |
| } |
| |
| // Set the idle state |
| setIdleState(false); |
| |
| if (V) { |
| Slog.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.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 handleWifiStateChanged(int wifiState) { |
| if (wifiState == WifiManager.WIFI_STATE_DISABLED) { |
| quit(); |
| } 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) { |
| DatagramSocket socket = null; |
| try { |
| 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) { |
| Slog.v(TAG, "DnsPinger.isReachable received SocketException", e); |
| } |
| return false; |
| |
| } catch (UnknownHostException e) { |
| if (V) { |
| Slog.v(TAG, "DnsPinger.isReachable is unable to resolve the DNS host", e); |
| } |
| return false; |
| |
| } catch (SocketTimeoutException e) { |
| return false; |
| |
| } catch (IOException e) { |
| if (V) { |
| Slog.v(TAG, "DnsPinger.isReachable got an IOException", e); |
| } |
| return false; |
| |
| } catch (Exception e) { |
| if (V || Config.LOGD) { |
| Slog.d(TAG, "DnsPinger.isReachable got an unknown exception", e); |
| } |
| return false; |
| } finally { |
| if (socket != null) { |
| socket.close(); |
| } |
| } |
| } |
| |
| 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); |
| } |
| } |
| } |
| } |