diff options
author | Isaac Levy <ilevy@google.com> | 2011-06-06 15:34:01 -0700 |
---|---|---|
committer | Isaac Levy <ilevy@google.com> | 2011-06-24 15:48:10 -0700 |
commit | bc7dfb58bffea133ccf6d94470a26f8d193f4890 (patch) | |
tree | f9a09b1b3db3e0c00b351b080cc5e3ae2941986d | |
parent | c1ba416af5d902ff9b9db7627ab73ec2baff427c (diff) | |
download | frameworks_base-bc7dfb58bffea133ccf6d94470a26f8d193f4890.zip frameworks_base-bc7dfb58bffea133ccf6d94470a26f8d193f4890.tar.gz frameworks_base-bc7dfb58bffea133ccf6d94470a26f8d193f4890.tar.bz2 |
WifiWatchdogService - disable bad connections
Complete rewrite of WifiWatchdogService.java. Checking for connectivity and managing wifi upon failure detection.
Change-Id: Ifcb8b5d7e0112cbc2f2282d76fdc93ea15527a44
-rw-r--r-- | services/java/com/android/server/ConnectivityService.java | 6 | ||||
-rw-r--r-- | services/java/com/android/server/DnsPinger.java | 188 | ||||
-rw-r--r-- | services/java/com/android/server/WifiService.java | 8 | ||||
-rw-r--r-- | services/java/com/android/server/WifiWatchdogService.java | 1788 | ||||
-rw-r--r-- | tests/CoreTests/MODULE_LICENSE_APACHE2 | 0 |
5 files changed, 747 insertions, 1243 deletions
diff --git a/services/java/com/android/server/ConnectivityService.java b/services/java/com/android/server/ConnectivityService.java index e6f443a..f3b5060 100644 --- a/services/java/com/android/server/ConnectivityService.java +++ b/services/java/com/android/server/ConnectivityService.java @@ -131,8 +131,6 @@ public class ConnectivityService extends IConnectivityManager.Stub { */ private List mNetRequestersPids[]; - private WifiWatchdogService mWifiWatchdogService; - // priority order of the nettrackers // (excluding dynamically set mNetworkPreference) // TODO - move mNetworkTypePreference into this @@ -432,10 +430,6 @@ public class ConnectivityService extends IConnectivityManager.Stub { wifiService.checkAndStartWifi(); mNetTrackers[ConnectivityManager.TYPE_WIFI] = wst; wst.startMonitoring(context, mHandler); - - //TODO: as part of WWS refactor, create only when needed - mWifiWatchdogService = new WifiWatchdogService(context); - break; case ConnectivityManager.TYPE_MOBILE: mNetTrackers[netType] = new MobileDataStateTracker(netType, diff --git a/services/java/com/android/server/DnsPinger.java b/services/java/com/android/server/DnsPinger.java new file mode 100644 index 0000000..5cfda7e --- /dev/null +++ b/services/java/com/android/server/DnsPinger.java @@ -0,0 +1,188 @@ +/* + * Copyright (C) 2011 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.ContentResolver; +import android.content.Context; +import android.net.ConnectivityManager; +import android.net.LinkProperties; +import android.os.SystemClock; +import android.util.Slog; + +import java.net.DatagramPacket; +import java.net.DatagramSocket; +import java.net.InetAddress; +import java.net.SocketTimeoutException; +import java.util.Collection; +import java.util.Random; + +/** + * 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). + * <p> + * TODO : More general API. Wifi is currently hard coded + * TODO : Choice of DNS query location - current looks up www.android.com + * + * @hide + */ +public final class DnsPinger { + private static final boolean V = true; + + /** 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(); + + private ConnectivityManager mConnectivityManager = null; + private ContentResolver mContentResolver; + private Context mContext; + + private String TAG; + + public DnsPinger(String TAG, Context context) { + mContext = context; + mContentResolver = context.getContentResolver(); + this.TAG = TAG; + } + + /** + * Gets the first DNS of the current Wifi AP. + * @return The first DNS of the current AP. + */ + public InetAddress getDns() { + if (mConnectivityManager == null) { + mConnectivityManager = (ConnectivityManager) mContext.getSystemService( + Context.CONNECTIVITY_SERVICE); + } + + LinkProperties linkProperties = mConnectivityManager.getLinkProperties( + ConnectivityManager.TYPE_WIFI); + if (linkProperties == null) + return null; + + Collection<InetAddress> dnses = linkProperties.getDnses(); + if (dnses == null || dnses.size() == 0) + return null; + + return dnses.iterator().next(); + } + + /** + * @return time to response. Negative value on error. + */ + public long pingDns(InetAddress dnsAddress, 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 + + DatagramPacket packet = new DatagramPacket(buf, + buf.length, dnsAddress, DNS_PORT); + long start = SystemClock.elapsedRealtime(); + 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 SystemClock.elapsedRealtime() - start; + + } catch (SocketTimeoutException e) { + // Squelch this exception. + return -1; + } catch (Exception e) { + if (V) { + Slog.v(TAG, "DnsPinger.pingDns got socket exception: ", e); + } + return -2; + } 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); + } + } +} diff --git a/services/java/com/android/server/WifiService.java b/services/java/com/android/server/WifiService.java index cb55451..7725891 100644 --- a/services/java/com/android/server/WifiService.java +++ b/services/java/com/android/server/WifiService.java @@ -342,6 +342,7 @@ public class WifiService extends IWifiManager.Stub { * Protected by mWifiStateTracker lock. */ private final WorkSource mTmpWorkSource = new WorkSource(); + private WifiWatchdogService mWifiWatchdogService; WifiService(Context context) { mContext = context; @@ -431,6 +432,9 @@ public class WifiService extends IWifiManager.Stub { Slog.i(TAG, "WifiService starting up with Wi-Fi " + (wifiEnabled ? "enabled" : "disabled")); setWifiEnabled(wifiEnabled); + + //TODO: as part of WWS refactor, create only when needed + mWifiWatchdogService = new WifiWatchdogService(mContext); } private boolean testAndClearWifiSavedState() { @@ -1155,6 +1159,10 @@ public class WifiService extends IWifiManager.Stub { pw.println(); pw.println("Locks held:"); mLocks.dump(pw); + + pw.println(); + pw.println("WifiWatchdogService dump"); + mWifiWatchdogService.dump(pw); } private class WifiLock extends DeathRecipient { diff --git a/services/java/com/android/server/WifiWatchdogService.java b/services/java/com/android/server/WifiWatchdogService.java index 56bfbe0..0b79478 100644 --- a/services/java/com/android/server/WifiWatchdogService.java +++ b/services/java/com/android/server/WifiWatchdogService.java @@ -22,1429 +22,743 @@ import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.database.ContentObserver; -import android.net.ConnectivityManager; -import android.net.LinkProperties; import android.net.NetworkInfo; +import android.net.Uri; import android.net.wifi.ScanResult; +import android.net.wifi.SupplicantState; import android.net.wifi.WifiInfo; import android.net.wifi.WifiManager; -import android.net.Uri; import android.os.Handler; +import android.os.HandlerThread; import android.os.Looper; import android.os.Message; +import android.os.SystemClock; import android.provider.Settings; import android.text.TextUtils; import android.util.Slog; import java.io.BufferedInputStream; -import java.io.InputStream; import java.io.IOException; -import java.net.DatagramPacket; -import java.net.DatagramSocket; +import java.io.InputStream; +import java.io.PrintWriter; import java.net.HttpURLConnection; -import java.net.InetAddress; -import java.net.SocketException; -import java.net.SocketTimeoutException; -import java.net.UnknownHostException; import java.net.URL; -import java.util.Collection; +import java.util.HashSet; import java.util.List; -import java.util.Random; import java.util.Scanner; /** * {@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. + * connects to an access point, the watchdog verifies connectivity by 'pinging' + * the configured DNS server using {@link DnsPinger}. * <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 + * On DNS check failure, the BSSID is blacklisted if it is reasonably likely + * that another AP might have internet access; otherwise the SSID is disabled. * <p> - * 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. + * On DNS success, the WatchdogService initiates a walled garden check via an + * http get. A browser windows is activated if a walled garden is detected. * - * {@hide} + * @hide */ public class WifiWatchdogService { - private static final String TAG = "WifiWatchdogService"; - private static final boolean V = false; - private static final boolean D = true; + + private static final String WWS_TAG = "WifiWatchdogService"; + + private static final boolean VDBG = true; + private static final boolean DBG = true; + + // Used for verbose logging + private String mDNSCheckLogStr; private Context mContext; private ContentResolver mContentResolver; private WifiManager mWifiManager; - private ConnectivityManager mConnectivityManager; - /** - * The main watchdog thread. - */ - private WifiWatchdogThread mThread; - /** - * The handler for the main watchdog thread. - */ private WifiWatchdogHandler mHandler; - private ContentObserver mContentObserver; + private DnsPinger mDnsPinger; - /** - * 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, using getter/setter methods. - */ - private int mBssidCheckCount; - /** Whether the current AP check should be canceled. */ - private boolean mShouldCancel; + private IntentFilter mIntentFilter; + private BroadcastReceiver mBroadcastReceiver; + private boolean mBroadcastsEnabled; - WifiWatchdogService(Context context) { - mContext = context; - mContentResolver = context.getContentResolver(); - 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"); - } - } + private static final int WIFI_SIGNAL_LEVELS = 4; /** - * Observes the watchdog on/off setting, and takes action when changed. + * Low signal is defined as less than or equal to cut off */ - 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(); - } - } - } - }); - } + private static final int LOW_SIGNAL_CUTOFF = 0; - /** - * @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); - } + private static final long MIN_LOW_SIGNAL_CHECK_INTERVAL = 2 * 60 * 1000; + private static final long MIN_SINGLE_DNS_CHECK_INTERVAL = 10 * 60 * 1000; + private static final long MIN_WALLED_GARDEN_INTERVAL = 15 * 60 * 1000; - /** - * @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); - } + private static final int MAX_CHECKS_PER_SSID = 7; + private static final int NUM_DNS_PINGS = 5; + private static double MIN_RESPONSE_RATE = 0.50; - /** - * @see android.provider.Settings.Secure#WIFI_WATCHDOG_WALLED_GARDEN_TEST_ENABLED - */ - private Boolean isWalledGardenTestEnabled() { - return Settings.Secure.getInt(mContentResolver, - Settings.Secure.WIFI_WATCHDOG_WALLED_GARDEN_TEST_ENABLED, 1) == 1; - } + // TODO : Adjust multiple DNS downward to 250 on repeated failure + // private static final int MULTI_DNS_PING_TIMEOUT_MS = 250; - /** - * @see android.provider.Settings.Secure#WIFI_WATCHDOG_WALLED_GARDEN_URL - */ - private String getWalledGardenUrl() { - String url = Settings.Secure.getString(mContentResolver, - Settings.Secure.WIFI_WATCHDOG_WALLED_GARDEN_URL); - if (TextUtils.isEmpty(url)) return "http://www.google.com/"; - return url; - } + private static final int DNS_PING_TIMEOUT_MS = 1000; + private static final long DNS_PING_INTERVAL = 250; - /** - * @see android.provider.Settings.Secure#WIFI_WATCHDOG_WALLED_GARDEN_PATTERN - */ - private String getWalledGardenPattern() { - String pattern = Settings.Secure.getString(mContentResolver, - Settings.Secure.WIFI_WATCHDOG_WALLED_GARDEN_PATTERN); - if (TextUtils.isEmpty(pattern)) return "<title>.*Google.*</title>"; - return pattern; - } - - /** - * @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); - } + private static final long BLACKLIST_FOLLOWUP_INTERVAL = 15 * 1000; - /** - * Unregisters from receiving the Wi-Fi broadcasts. - */ - private void unregisterForWifiBroadcasts() { - mContext.unregisterReceiver(mReceiver); - } + private Status mStatus = new Status(); - /** - * Creates the main watchdog thread, including waiting for the handler to be - * created. - */ - private void createThread() { - mThread = new WifiWatchdogThread(); - mThread.start(); - waitForHandlerCreation(); - } + private static class Status { + String bssid = ""; + String ssid = ""; - /** - * Unregister broadcasts and quit the watchdog thread - */ - //TODO: Change back to running WWS when needed -// private void quit() { -// unregisterForWifiBroadcasts(); -// mContext.getContentResolver().unregisterContentObserver(mContentObserver); -// mHandler.removeAllActions(); -// mHandler.getLooper().quit(); -// } + HashSet<String> allBssids = new HashSet<String>(); + int numFullDNSchecks = 0; - /** - * 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."); - } - } - } - } + long lastSingleCheckTime = -24 * 60 * 60 * 1000; + long lastWalledGardenCheckTime = -24 * 60 * 60 * 1000; - // 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 first DNS of the current AP. - * - * @return The first DNS of the current AP. - */ - private InetAddress getDns() { - if (mConnectivityManager == null) { - mConnectivityManager = (ConnectivityManager)mContext.getSystemService( - Context.CONNECTIVITY_SERVICE); - } + WatchdogState state = WatchdogState.INACTIVE; - LinkProperties linkProperties = mConnectivityManager.getLinkProperties( - ConnectivityManager.TYPE_WIFI); - if (linkProperties == null) return null; + // Info for dns check + int dnsCheckTries = 0; + int dnsCheckSuccesses = 0; - Collection<InetAddress> dnses = linkProperties.getDnses(); - if (dnses == null || dnses.size() == 0) return null; + public int signal = -200; - return dnses.iterator().next(); } - /** - * 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() { - InetAddress dns = getDns(); - if (dns == null) { - if (V) { - myLogV("checkDnsConnectivity: Invalid DNS, returning false"); - } - return false; - } - - if (V) { - myLogV("checkDnsConnectivity: Checking " + dns.getHostAddress() + " 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; + private enum WatchdogState { + /** + * Full DNS check in progress + */ + DNS_FULL_CHECK, - // No connectivity check needed - if (numPings == 0) { - return true; - } + /** + * Walled Garden detected, will pop up browser next round. + */ + WALLED_GARDEN_DETECTED, - // 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. - */ - - // TODO: This is confusing logic and should be rewitten - // Here, successful 'ignored' pings are interpreted as a success in the below loop - pingCounter++; - successCounter++; - } + /** + * DNS failed, will blacklist/disable AP next round + */ + DNS_CHECK_FAILURE, - if (V) { - Slog.v(TAG, (dnsAlive ? " +" : " Ignored: -")); - } + /** + * Online or displaying walled garden auth page + */ + CHECKS_COMPLETE, - if (shouldCancel()) return false; + /** + * Watchdog idle, network has been blacklisted or received disconnect + * msg + */ + INACTIVE, - try { - Thread.sleep(pingDelay); - } catch (InterruptedException e) { - Slog.w(TAG, "Interrupted while pausing between pings", e); - } - } + BLACKLISTED_AP + } - // Do the pings that we use to measure packet loss - for (; pingCounter < numPings; pingCounter++) { - if (shouldCancel()) return false; + WifiWatchdogService(Context context) { + mContext = context; + mContentResolver = context.getContentResolver(); + mWifiManager = (WifiManager) context.getSystemService(Context.WIFI_SERVICE); + mDnsPinger = new DnsPinger("WifiWatchdogServer.DnsPinger", context); - if (DnsPinger.isDnsReachable(dns, getPingTimeoutMs())) { - successCounter++; - if (V) { - Slog.v(TAG, " +"); - } - } else { - if (V) { - Slog.v(TAG, " -"); - } - } + HandlerThread handlerThread = new HandlerThread("WifiWatchdogServiceThread"); + handlerThread.start(); + mHandler = new WifiWatchdogHandler(handlerThread.getLooper()); - if (shouldCancel()) return false; + setupNetworkReceiver(); - try { - Thread.sleep(pingDelay); - } catch (InterruptedException e) { - Slog.w(TAG, "Interrupted while pausing between pings", e); - } - } + // The content observer to listen needs a handler, which createThread + // creates + registerForSettingsChanges(); - //TODO: Integer division might cause problems down the road... - int packetLossPercentage = 100 * (numPings - successCounter) / numPings; - if (D) { - Slog.d(TAG, packetLossPercentage - + "% packet loss (acceptable is " + acceptableLoss + "%)"); + // Start things off + if (isWatchdogEnabled()) { + mHandler.sendEmptyMessage(WifiWatchdogHandler.MESSAGE_CONTEXT_EVENT); } - - return !shouldCancel() && (packetLossPercentage <= acceptableLoss); } - private boolean backgroundCheckDnsConnectivity() { - InetAddress dns = getDns(); - - if (dns == null) { - if (V) { - myLogV("backgroundCheckDnsConnectivity: DNS is empty, returning false"); + /** + * + */ + private void setupNetworkReceiver() { + mBroadcastReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + String action = intent.getAction(); + if (action.equals(WifiManager.NETWORK_STATE_CHANGED_ACTION)) { + mHandler.sendMessage(mHandler.obtainMessage( + WifiWatchdogHandler.MESSAGE_NETWORK_EVENT, + intent.getParcelableExtra(WifiManager.EXTRA_NETWORK_INFO) + )); + } else if (action.equals(WifiManager.RSSI_CHANGED_ACTION)) { + mHandler.sendEmptyMessage(WifiWatchdogHandler.RSSI_CHANGE_EVENT); + } else if (action.equals(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION)) { + mHandler.sendEmptyMessage(WifiWatchdogHandler.SCAN_RESULTS_AVAILABLE); + } else if (action.equals(WifiManager.WIFI_STATE_CHANGED_ACTION)) { + mHandler.sendMessage(mHandler.obtainMessage( + WifiWatchdogHandler.WIFI_STATE_CHANGE, + intent.getIntExtra(WifiManager.EXTRA_WIFI_STATE, 4))); + } } - return false; - } + }; - if (V) { - myLogV("backgroundCheckDnsConnectivity: Background checking " + - dns.getHostAddress() + " for connectivity"); - } - - return DnsPinger.isDnsReachable(dns, getBackgroundCheckTimeoutMs()); + mIntentFilter = new IntentFilter(); + mIntentFilter.addAction(WifiManager.NETWORK_STATE_CHANGED_ACTION); + mIntentFilter.addAction(WifiManager.WIFI_STATE_CHANGED_ACTION); + mIntentFilter.addAction(WifiManager.RSSI_CHANGED_ACTION); + mIntentFilter.addAction(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION); } /** - * Signals the current action to cancel. + * Observes the watchdog on/off setting, and takes action when changed. */ - private void cancelCurrentAction() { - mShouldCancel = true; + private void registerForSettingsChanges() { + ContentObserver contentObserver = new ContentObserver(mHandler) { + @Override + public void onChange(boolean selfChange) { + mHandler.sendEmptyMessage((WifiWatchdogHandler.MESSAGE_CONTEXT_EVENT)); + } + }; + + mContext.getContentResolver().registerContentObserver( + Settings.Secure.getUriFor(Settings.Secure.WIFI_WATCHDOG_ON), + false, contentObserver); } - - /** - * Helper to check whether to cancel. - * - * @return Whether to cancel processing the action. - */ - private boolean shouldCancel() { - if (V && mShouldCancel) { - myLogV("shouldCancel: Cancelling"); + + private void handleNewConnection() { + WifiInfo wifiInfo = mWifiManager.getConnectionInfo(); + String newSsid = wifiInfo.getSSID(); + String newBssid = wifiInfo.getBSSID(); + + if (VDBG) { + Slog.v(WWS_TAG, String.format("handleConnected:: old (%s, %s) ==> new (%s, %s)", + mStatus.ssid, mStatus.bssid, newSsid, newBssid)); } - - 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); + if (TextUtils.isEmpty(newSsid) || TextUtils.isEmpty(newBssid)) { + return; } - /* - * 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 (!TextUtils.equals(mStatus.ssid, newSsid)) { + mStatus = new Status(); + mStatus.ssid = newSsid; } - - 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"); - } + mStatus.bssid = newBssid; + mStatus.allBssids.add(newBssid); + mStatus.signal = WifiManager.calculateSignalLevel(wifiInfo.getRssi(), WIFI_SIGNAL_LEVELS); - // This access point does not require a watchdog, so queue idle on the main thread - mHandler.idle(); - } - if (isWalledGardenTestEnabled()) mHandler.checkWalledGarden(ssid); + initDnsFullCheck(); } - - /** - * 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"); + + public void updateRssi() { + WifiInfo wifiInfo = mWifiManager.getConnectionInfo(); + if (!TextUtils.equals(mStatus.ssid, wifiInfo.getSSID()) || + !TextUtils.equals(mStatus.bssid, wifiInfo.getBSSID())) { + return; } - - /* - * 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(); + + mStatus.signal = WifiManager.calculateSignalLevel(wifiInfo.getRssi(), WIFI_SIGNAL_LEVELS); } /** - * 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. + * Single step in state machine */ - 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; - } + private void handleStateStep() { + // Slog.v(WWS_TAG, "handleStateStep:: " + mStatus.state); - // The watchdog only monitors networks with multiple APs - if (!hasRequiredNumberOfAps(ssid)) { - return false; - } - - return true; - } + switch (mStatus.state) { + case DNS_FULL_CHECK: + if (VDBG) { + Slog.v(WWS_TAG, "DNS_FULL_CHECK: " + mDNSCheckLogStr); + } - private boolean isOnWatchList(String ssid) { - String watchList; + long pingResponseTime = mDnsPinger.pingDns(mDnsPinger.getDns(), + DNS_PING_TIMEOUT_MS); - if (ssid == null || (watchList = getWatchList()) == null) { - return false; - } + mStatus.dnsCheckTries++; + if (pingResponseTime >= 0) + mStatus.dnsCheckSuccesses++; - String[] list = watchList.split(" *, *"); + if (DBG) { + if (pingResponseTime >= 0) { + mDNSCheckLogStr += " | " + pingResponseTime; + } else { + mDNSCheckLogStr += " | " + "x"; + } + } - for (String name : list) { - if (ssid.equals(name)) { - return true; - } - } + switch (currentDnsCheckStatus()) { + case SUCCESS: + if (DBG) { + Slog.d(WWS_TAG, mDNSCheckLogStr + " -- Success"); + } + doWalledGardenCheck(); + break; + case FAILURE: + if (DBG) { + Slog.d(WWS_TAG, mDNSCheckLogStr + " -- Failure"); + } + mStatus.state = WatchdogState.DNS_CHECK_FAILURE; + break; + case INCOMPLETE: + // Taking no action + break; + } + break; + case DNS_CHECK_FAILURE: + WifiInfo wifiInfo = mWifiManager.getConnectionInfo(); + if (!mStatus.ssid.equals(wifiInfo.getSSID()) || + !mStatus.bssid.equals(wifiInfo.getBSSID())) { + Slog.i(WWS_TAG, "handleState DNS_CHECK_FAILURE:: network has changed!"); + mStatus.state = WatchdogState.INACTIVE; + break; + } - 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"); + if (mStatus.numFullDNSchecks >= mStatus.allBssids.size() || + mStatus.numFullDNSchecks >= MAX_CHECKS_PER_SSID) { + disableAP(wifiInfo); + } else { + blacklistAP(); + } + break; + case WALLED_GARDEN_DETECTED: + popUpBrowser(); + mStatus.state = WatchdogState.CHECKS_COMPLETE; + break; + case BLACKLISTED_AP: + WifiInfo wifiInfo2 = mWifiManager.getConnectionInfo(); + if (wifiInfo2.getSupplicantState() != SupplicantState.COMPLETED) { + Slog.d(WWS_TAG, + "handleState::BlacklistedAP - offline, but didn't get disconnect!"); + mStatus.state = WatchdogState.INACTIVE; + break; + } + if (mStatus.bssid.equals(wifiInfo2.getBSSID())) { + Slog.d(WWS_TAG, "handleState::BlacklistedAP - connected to same bssid"); + if (!handleSingleDnsCheck()) { + disableAP(wifiInfo2); + break; } - return true; } - } - } - - if (V) { - myLogV("hasRequiredNumberOfAps: SSID: " + ssid + ", returning false"); + + Slog.d(WWS_TAG, "handleState::BlacklistedAP - Simiulating a new connection"); + handleNewConnection(); + break; } - 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"); - } + + private void doWalledGardenCheck() { + if (!isWalledGardenTestEnabled()) { + if (VDBG) + Slog.v(WWS_TAG, "Skipping walled garden check - disabled"); + mStatus.state = WatchdogState.CHECKS_COMPLETE; return; } - - mState = WatchdogState.CHECKING_AP; - - /* - * Checks to make sure we haven't exceeded the max number of checks - * we're allowed per network - */ - incrementBssidCheckCount(); - if (getBssidCheckCount() > getMaxApChecks()) { - if (V) { - Slog.v(TAG, " Passed the max attempts (" + getMaxApChecks() - + "), going to sleep for " + mSsid); + long waitTime = waitTime(MIN_WALLED_GARDEN_INTERVAL, + mStatus.lastWalledGardenCheckTime); + if (waitTime > 0) { + if (VDBG) { + Slog.v(WWS_TAG, "Skipping walled garden check - wait " + + waitTime + " ms."); } - mHandler.sleep(mSsid); + mStatus.state = WatchdogState.CHECKS_COMPLETE; return; } - // Do the check - boolean isApAlive = checkDnsConnectivity(); - - if (V) { - Slog.v(TAG, " Is it alive: " + isApAlive); + mStatus.lastWalledGardenCheckTime = SystemClock.elapsedRealtime(); + if (isWalledGardenConnection()) { + if (DBG) + Slog.d(WWS_TAG, + "Walled garden test complete - walled garden detected"); + mStatus.state = WatchdogState.WALLED_GARDEN_DETECTED; + } else { + if (DBG) + Slog.d(WWS_TAG, "Walled garden test complete - online"); + mStatus.state = WatchdogState.CHECKS_COMPLETE; } + } - // Take action based on results - if (isApAlive) { - handleApAlive(ap); - } else { - handleApUnresponsive(ap); + private boolean handleSingleDnsCheck() { + mStatus.lastSingleCheckTime = SystemClock.elapsedRealtime(); + long responseTime = mDnsPinger.pingDns(mDnsPinger.getDns(), + DNS_PING_TIMEOUT_MS); + if (DBG) { + Slog.d(WWS_TAG, "Ran a single DNS ping. Response time: " + responseTime); } + if (responseTime < 0) { + return false; + } + return true; + } /** - * Handles the case when an access point is alive. - * - * @param ap The access point. + * @return Delay in MS before next single DNS check can proceed. */ - 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()); + private long timeToNextScheduledDNSCheck() { + if (mStatus.signal > LOW_SIGNAL_CUTOFF) { + return waitTime(MIN_SINGLE_DNS_CHECK_INTERVAL, mStatus.lastSingleCheckTime); + } else { + return waitTime(MIN_LOW_SIGNAL_CHECK_INTERVAL, mStatus.lastSingleCheckTime); } - - // Queue the next action to be a background check - mHandler.backgroundCheckAp(ap); } - + /** - * Handles an unresponsive AP by blacklisting it. + * Helper to return wait time left given a min interval and last run * - * @param ap The access point. + * @param interval minimum wait interval + * @param lastTime last time action was performed in + * SystemClock.elapsedRealtime() + * @return non negative time to wait */ - 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 - mWifiManager.reassociate(); + private static long waitTime(long interval, long lastTime) { + long wait = interval + lastTime - SystemClock.elapsedRealtime(); + return wait > 0 ? wait : 0; } - private void blacklistAp(String bssid) { - if (TextUtils.isEmpty(bssid)) { - return; - } - - // Before taking action, make sure we should not cancel our processing - if (shouldCancel()) return; - - mWifiManager.addToBlacklist(bssid); - - if (D) { - myLogD("Blacklisting " + bssid); - } + private void popUpBrowser() { + Uri uri = Uri.parse("http://www.google.com"); + Intent intent = new Intent(Intent.ACTION_VIEW, uri); + intent.setFlags(Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT | + Intent.FLAG_ACTIVITY_NEW_TASK); + mContext.startActivity(intent); + } + + private void disableAP(WifiInfo info) { + // TODO : Unban networks if they had low signal ? + Slog.i(WWS_TAG, String.format("Disabling current SSID, %s [bssid %s]. " + + "numChecks %d, numAPs %d", mStatus.ssid, mStatus.bssid, + mStatus.numFullDNSchecks, mStatus.allBssids.size())); + mWifiManager.disableNetwork(info.getNetworkId()); + mStatus.state = WatchdogState.INACTIVE; + } + + private void blacklistAP() { + Slog.i(WWS_TAG, String.format("Blacklisting current BSSID %s [ssid %s]. " + + "numChecks %d, numAPs %d", mStatus.bssid, mStatus.ssid, + mStatus.numFullDNSchecks, mStatus.allBssids.size())); + + mWifiManager.addToBlacklist(mStatus.bssid); + mWifiManager.reassociate(); + mStatus.state = WatchdogState.BLACKLISTED_AP; } /** - * 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. + * Checks the scan for new BBIDs using current mSsid */ - private void handleBackgroundCheckAp(AccessPoint ap) { - // Reset the cancel state since this is the entry point of this action - mShouldCancel = false; - - if (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); + private void updateBssids() { + String curSsid = mStatus.ssid; + HashSet<String> bssids = mStatus.allBssids; + List<ScanResult> results = mWifiManager.getScanResults(); + int oldNumBssids = bssids.size(); + + if (results == null) { + if (VDBG) { + Slog.v(WWS_TAG, "updateBssids: Got null scan results!"); } return; } - // Do the check - boolean isApAlive = backgroundCheckDnsConnectivity(); - - if (V && !isApAlive) { - Slog.v(TAG, " handleBackgroundCheckAp: Is it alive: " + isApAlive); + for (ScanResult result : results) { + if (result != null && curSsid.equals(result.SSID)) + bssids.add(result.BSSID); } - 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); - } + // if (VDBG && bssids.size() - oldNumBssids > 0) { + // Slog.v(WWS_TAG, + // String.format("updateBssids:: Found %d new APs (total %d) on SSID %s", + // bssids.size() - oldNumBssids, bssids.size(), curSsid)); + // } } - - /** - * 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. - */ - mWifiManager.clearBlacklist(); - - if (V) { - myLogV("handleSleep: Set state to SLEEP and cleared blacklist"); - } - } + enum DnsCheckStatus { + SUCCESS, + FAILURE, + INCOMPLETE } /** - * Handles an access point disconnection. + * Computes the current results of the dns check, ends early if outcome is + * assured. */ - 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. + private DnsCheckStatus currentDnsCheckStatus() { + /** + * After a full ping count, if we have more responses than this cutoff, + * the outcome is success; else it is 'failure'. */ - setIdleState(false); - } + double pingResponseCutoff = MIN_RESPONSE_RATE * NUM_DNS_PINGS; + int remainingChecks = NUM_DNS_PINGS - mStatus.dnsCheckTries; - /** - * 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; + /** + * Our final success count will be at least this big, so we're + * guaranteed to succeed. + */ + if (mStatus.dnsCheckSuccesses >= pingResponseCutoff) { + return DnsCheckStatus.SUCCESS; } - resetBssidCheckCount(); - } - - /** - * 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() { - mWifiManager.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 - } + /** + * Our final count will be at most the current count plus the remaining + * pings - we're guaranteed to fail. + */ + if (remainingChecks + mStatus.dnsCheckSuccesses < pingResponseCutoff) { + return DnsCheckStatus.FAILURE; + } - private int getBssidCheckCount() { - return mBssidCheckCount; + return DnsCheckStatus.INCOMPLETE; } - private void incrementBssidCheckCount() { - mBssidCheckCount++; - } + private void initDnsFullCheck() { + if (DBG) { + Slog.d(WWS_TAG, "Starting DNS pings at " + SystemClock.elapsedRealtime()); + } + mStatus.numFullDNSchecks++; + mStatus.dnsCheckSuccesses = 0; + mStatus.dnsCheckTries = 0; + mStatus.state = WatchdogState.DNS_FULL_CHECK; - private void resetBssidCheckCount() { - this.mBssidCheckCount = 0; + if (DBG) { + mDNSCheckLogStr = String.format("Dns Check %d. Pinging %s on ssid [%s]: ", + mStatus.numFullDNSchecks, mDnsPinger.getDns().getHostAddress(), + mStatus.ssid); + } } /** - * The main thread for the watchdog monitoring. This will be turned into a - * {@link Looper} thread. + * DNS based detection techniques do not work at all hotspots. The one sure + * way to check a walled garden is to see if a URL fetch on a known address + * fetches the data we expect */ - 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(); + private boolean isWalledGardenConnection() { + InputStream in = null; + HttpURLConnection urlConnection = null; + try { + URL url = new URL(getWalledGardenUrl()); + urlConnection = (HttpURLConnection) url.openConnection(); + in = new BufferedInputStream(urlConnection.getInputStream()); + Scanner scanner = new Scanner(in); + if (scanner.findInLine(getWalledGardenPattern()) != null) { + return false; + } else { + return true; } - - // Listen for messages to the handler - Looper.loop(); + } catch (IOException e) { + return false; + } finally { + if (in != null) { + try { + in.close(); + } catch (IOException e) { + } + } + if (urlConnection != null) + urlConnection.disconnect(); } } /** - * 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}. + * Major network event, object is NetworkInfo */ - static final int ACTION_BACKGROUND_CHECK_AP = 3; - /** Check whether the connection is a walled garden */ - static final int ACTION_CHECK_WALLED_GARDEN = 4; - + static final int MESSAGE_NETWORK_EVENT = 1; /** - * 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}. + * Change in settings, no object */ - 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; - - /* Walled garden detection */ - private String mLastSsid; - private long mLastTime; - private final long MIN_WALLED_GARDEN_TEST_INTERVAL = 15 * 60 * 1000; //15 minutes - - void checkWalledGarden(String ssid) { - sendMessage(obtainMessage(ACTION_CHECK_WALLED_GARDEN, ssid)); - } + static final int MESSAGE_CONTEXT_EVENT = 2; - 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)); - } + /** + * Change in signal strength + */ + static final int RSSI_CHANGE_EVENT = 3; + static final int SCAN_RESULTS_AVAILABLE = 4; - void dispatchDisconnected() { - removeAllActions(); - sendMessage(obtainMessage(MESSAGE_DISCONNECTED)); - } + static final int WIFI_STATE_CHANGE = 5; - 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) { - if (V) { - myLogV("handleMessage: " + msg.what); - } - 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 ACTION_CHECK_WALLED_GARDEN: - handleWalledGardenCheck((String) 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; - } - } + /** + * Single step of state machine. One DNS check, or one WalledGarden + * check, or one external action. We separate out external actions to + * increase chance of detecting that a check failure is caused by change + * in network status. Messages should have an arg1 which to sync status + * messages. + */ + static final int CHECK_SEQUENCE_STEP = 10; + static final int SINGLE_DNS_CHECK = 11; /** - * DNS based detection techniques do not work at all hotspots. The one sure way to check - * a walled garden is to see if a URL fetch on a known address fetches the data we - * expect + * @param looper */ - private boolean isWalledGardenConnection() { - InputStream in = null; - HttpURLConnection urlConnection = null; - try { - URL url = new URL(getWalledGardenUrl()); - urlConnection = (HttpURLConnection) url.openConnection(); - in = new BufferedInputStream(urlConnection.getInputStream()); - Scanner scanner = new Scanner(in); - if (scanner.findInLine(getWalledGardenPattern()) != null) { - return false; - } else { - return true; - } - } catch (IOException e) { - return false; - } finally { - if (in != null) { - try { - in.close(); - } catch (IOException e) { - } - } - if (urlConnection != null) urlConnection.disconnect(); - } + public WifiWatchdogHandler(Looper looper) { + super(looper); } - private void handleWalledGardenCheck(String ssid) { - long currentTime = System.currentTimeMillis(); - //Avoid a walled garden test on the same network if one was already done - //within MIN_WALLED_GARDEN_TEST_INTERVAL. This will handle scenarios where - //there are frequent network disconnections - if (ssid.equals(mLastSsid) && - (currentTime - mLastTime) < MIN_WALLED_GARDEN_TEST_INTERVAL) { - return; - } - - mLastTime = currentTime; - mLastSsid = ssid; + boolean singleCheckQueued = false; + long queuedSingleDnsCheckArrival; - if (isWalledGardenConnection()) { - Uri uri = Uri.parse("http://www.google.com"); - Intent intent = new Intent(Intent.ACTION_VIEW, uri); - intent.setFlags(Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT | - Intent.FLAG_ACTIVITY_NEW_TASK); - mContext.startActivity(intent); - } + /** + * Sends a singleDnsCheck message with shortest time - guards against + * multiple. + */ + private boolean queueSingleDnsCheck() { + long delay = timeToNextScheduledDNSCheck(); + long newArrival = delay + SystemClock.elapsedRealtime(); + if (singleCheckQueued && queuedSingleDnsCheckArrival <= newArrival) + return true; + queuedSingleDnsCheckArrival = newArrival; + singleCheckQueued = true; + removeMessages(SINGLE_DNS_CHECK); + return sendMessageDelayed(obtainMessage(SINGLE_DNS_CHECK), delay); } - } - /** - * 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() { + boolean checkSequenceQueued = false; + long queuedCheckSequenceArrival; - @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)); - } + /** + * Sends a state_machine_step message if the delay requested is lower + * than the current delay. + */ + private boolean sendCheckSequenceStep(long delay) { + long newArrival = delay + SystemClock.elapsedRealtime(); + if (checkSequenceQueued && queuedCheckSequenceArrival <= newArrival) + return true; + queuedCheckSequenceArrival = newArrival; + checkSequenceQueued = true; + removeMessages(CHECK_SEQUENCE_STEP); + return sendMessageDelayed(obtainMessage(CHECK_SEQUENCE_STEP), delay); } - 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"); - } + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case CHECK_SEQUENCE_STEP: + checkSequenceQueued = false; + handleStateStep(); + if (mStatus.state == WatchdogState.CHECKS_COMPLETE) { + queueSingleDnsCheck(); + } else if (mStatus.state == WatchdogState.DNS_FULL_CHECK) { + sendCheckSequenceStep(DNS_PING_INTERVAL); + } else if (mStatus.state == WatchdogState.BLACKLISTED_AP) { + sendCheckSequenceStep(BLACKLIST_FOLLOWUP_INTERVAL); + } else if (mStatus.state != WatchdogState.INACTIVE) { + sendCheckSequenceStep(0); + } + return; + case MESSAGE_NETWORK_EVENT: + if (!mBroadcastsEnabled) { + Slog.e(WWS_TAG, + "MessageNetworkEvent - WatchdogService not enabled... returning"); return; } - onConnected(wifiInfo.getSSID(), wifiInfo.getBSSID()); - break; + NetworkInfo info = (NetworkInfo) msg.obj; + switch (info.getState()) { + case DISCONNECTED: + mStatus.state = WatchdogState.INACTIVE; + return; + case CONNECTED: + handleNewConnection(); + sendCheckSequenceStep(0); + } + return; + case SINGLE_DNS_CHECK: + singleCheckQueued = false; + if (mStatus.state != WatchdogState.CHECKS_COMPLETE) { + Slog.d(WWS_TAG, "Single check returning, curState: " + mStatus.state); + break; + } + + if (!handleSingleDnsCheck()) { + initDnsFullCheck(); + sendCheckSequenceStep(0); + } else { + queueSingleDnsCheck(); + } - case DISCONNECTED: - onDisconnected(); + break; + case RSSI_CHANGE_EVENT: + updateRssi(); + if (mStatus.state == WatchdogState.CHECKS_COMPLETE) + queueSingleDnsCheck(); + break; + case SCAN_RESULTS_AVAILABLE: + updateBssids(); + break; + case WIFI_STATE_CHANGE: + if ((Integer) msg.obj == WifiManager.WIFI_STATE_DISABLING) { + Slog.i(WWS_TAG, "WifiStateDisabling -- Resetting WatchdogState"); + mStatus = new Status(); + } + break; + case MESSAGE_CONTEXT_EVENT: + if (isWatchdogEnabled() && !mBroadcastsEnabled) { + mContext.registerReceiver(mBroadcastReceiver, mIntentFilter); + mBroadcastsEnabled = true; + Slog.i(WWS_TAG, "WifiWatchdogService enabled"); + } else if (!isWatchdogEnabled() && mBroadcastsEnabled) { + mContext.unregisterReceiver(mBroadcastReceiver); + removeMessages(SINGLE_DNS_CHECK); + removeMessages(CHECK_SEQUENCE_STEP); + mBroadcastsEnabled = false; + Slog.i(WWS_TAG, "WifiWatchdogService disabled"); + } break; } } + } - private void handleWifiStateChanged(int wifiState) { - if (wifiState == WifiManager.WIFI_STATE_DISABLED) { - onDisconnected(); - } else if (wifiState == WifiManager.WIFI_STATE_ENABLED) { - onEnabled(); - } - } - }; + public void dump(PrintWriter pw) { + pw.print("WatchdogStatus: "); + pw.print("State " + mStatus.state); + pw.println(", network [" + mStatus.ssid + ", " + mStatus.bssid + "]"); + pw.print("checkCount " + mStatus.numFullDNSchecks); + pw.print(", bssids: " + mStatus.allBssids.size()); + pw.print(", hasCheckMessages? " + + mHandler.hasMessages(WifiWatchdogHandler.CHECK_SEQUENCE_STEP)); + pw.println(" hasSingleCheckMessages? " + + mHandler.hasMessages(WifiWatchdogHandler.SINGLE_DNS_CHECK)); + } /** - * Describes an access point by its SSID and BSSID. - * + * @see android.provider.Settings.Secure#WIFI_WATCHDOG_WALLED_GARDEN_TEST_ENABLED */ - private static class AccessPoint { - String ssid; - String bssid; - - /** - * @param ssid cannot be null - * @param bssid cannot be null - */ - AccessPoint(String ssid, String bssid) { - if (ssid == null || bssid == null) { - Slog.e(TAG, String.format("(%s) INVALID ACCESSPOINT: (%s, %s)", - Thread.currentThread().getName(),ssid,bssid)); - } - this.ssid = ssid; - this.bssid = bssid; - } - - @Override - public boolean equals(Object o) { - if (!(o instanceof AccessPoint)) return false; - AccessPoint otherAp = (AccessPoint) o; - - // Either we both have a null, or our SSIDs and BSSIDs are equal - return ssid.equals(otherAp.ssid) && bssid.equals(otherAp.bssid); - } - - @Override - public int hashCode() { - return ssid.hashCode() + bssid.hashCode(); - } - - @Override - public String toString() { - return ssid + " (" + bssid + ")"; - } + private Boolean isWalledGardenTestEnabled() { + return Settings.Secure.getInt(mContentResolver, + Settings.Secure.WIFI_WATCHDOG_WALLED_GARDEN_TEST_ENABLED, 1) == 1; } /** - * 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). + * @see android.provider.Settings.Secure#WIFI_WATCHDOG_WALLED_GARDEN_URL */ - 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(InetAddress dnsAddress, 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 - - 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; + private String getWalledGardenUrl() { + String url = Settings.Secure.getString(mContentResolver, + Settings.Secure.WIFI_WATCHDOG_WALLED_GARDEN_URL); + if (TextUtils.isEmpty(url)) + return "http://www.google.com/"; + return url; + } - } catch (UnknownHostException e) { - if (V) { - Slog.v(TAG, "DnsPinger.isReachable is unable to resolve the DNS host", e); - } - return false; + /** + * @see android.provider.Settings.Secure#WIFI_WATCHDOG_WALLED_GARDEN_PATTERN + */ + private String getWalledGardenPattern() { + String pattern = Settings.Secure.getString(mContentResolver, + Settings.Secure.WIFI_WATCHDOG_WALLED_GARDEN_PATTERN); + if (TextUtils.isEmpty(pattern)) + return "<title>.*Google.*</title>"; + return pattern; + } - } 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) { - 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); - } - } + /** + * @see android.provider.Settings.Secure#WIFI_WATCHDOG_ON + */ + private boolean isWatchdogEnabled() { + return Settings.Secure.getInt(mContentResolver, + Settings.Secure.WIFI_WATCHDOG_ON, 1) == 1; } } diff --git a/tests/CoreTests/MODULE_LICENSE_APACHE2 b/tests/CoreTests/MODULE_LICENSE_APACHE2 deleted file mode 100644 index e69de29..0000000 --- a/tests/CoreTests/MODULE_LICENSE_APACHE2 +++ /dev/null |