/* * 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.ConnectivityManager; 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.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.IOException; import java.io.InputStream; import java.io.PrintWriter; import java.net.HttpURLConnection; import java.net.URL; import java.util.HashSet; import java.util.List; 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 connectivity by 'pinging' * the configured DNS server using {@link DnsPinger}. *

* 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. *

* 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 */ public class WifiWatchdogService { 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 WifiWatchdogHandler mHandler; private DnsPinger mDnsPinger; private IntentFilter mIntentFilter; private BroadcastReceiver mBroadcastReceiver; private boolean mBroadcastsEnabled; private static final int WIFI_SIGNAL_LEVELS = 4; /** * Low signal is defined as less than or equal to cut off */ private static final int LOW_SIGNAL_CUTOFF = 0; 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; private static final int MAX_CHECKS_PER_SSID = 9; private static final int NUM_DNS_PINGS = 7; private static double MIN_RESPONSE_RATE = 0.50; // TODO : Adjust multiple DNS downward to 250 on repeated failure // private static final int MULTI_DNS_PING_TIMEOUT_MS = 250; private static final int DNS_PING_TIMEOUT_MS = 800; private static final long DNS_PING_INTERVAL = 250; private static final long BLACKLIST_FOLLOWUP_INTERVAL = 15 * 1000; private Status mStatus = new Status(); private static class Status { String bssid = ""; String ssid = ""; HashSet allBssids = new HashSet(); int numFullDNSchecks = 0; long lastSingleCheckTime = -24 * 60 * 60 * 1000; long lastWalledGardenCheckTime = -24 * 60 * 60 * 1000; WatchdogState state = WatchdogState.INACTIVE; // Info for dns check int dnsCheckTries = 0; int dnsCheckSuccesses = 0; public int signal = -200; } private enum WatchdogState { /** * Full DNS check in progress */ DNS_FULL_CHECK, /** * Walled Garden detected, will pop up browser next round. */ WALLED_GARDEN_DETECTED, /** * DNS failed, will blacklist/disable AP next round */ DNS_CHECK_FAILURE, /** * Online or displaying walled garden auth page */ CHECKS_COMPLETE, /** * Watchdog idle, network has been blacklisted or received disconnect * msg */ INACTIVE, BLACKLISTED_AP } WifiWatchdogService(Context context) { mContext = context; mContentResolver = context.getContentResolver(); mWifiManager = (WifiManager) context.getSystemService(Context.WIFI_SERVICE); mDnsPinger = new DnsPinger("WifiWatchdogServer.DnsPinger", context, ConnectivityManager.TYPE_WIFI); HandlerThread handlerThread = new HandlerThread("WifiWatchdogServiceThread"); handlerThread.start(); mHandler = new WifiWatchdogHandler(handlerThread.getLooper()); setupNetworkReceiver(); // The content observer to listen needs a handler, which createThread // creates registerForSettingsChanges(); // Start things off if (isWatchdogEnabled()) { mHandler.sendEmptyMessage(WifiWatchdogHandler.MESSAGE_CONTEXT_EVENT); } } /** * */ 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))); } } }; 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); } /** * Observes the watchdog on/off setting, and takes action when changed. */ 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); } 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)); } if (TextUtils.isEmpty(newSsid) || TextUtils.isEmpty(newBssid)) { return; } if (!TextUtils.equals(mStatus.ssid, newSsid)) { mStatus = new Status(); mStatus.ssid = newSsid; } mStatus.bssid = newBssid; mStatus.allBssids.add(newBssid); mStatus.signal = WifiManager.calculateSignalLevel(wifiInfo.getRssi(), WIFI_SIGNAL_LEVELS); initDnsFullCheck(); } public void updateRssi() { WifiInfo wifiInfo = mWifiManager.getConnectionInfo(); if (!TextUtils.equals(mStatus.ssid, wifiInfo.getSSID()) || !TextUtils.equals(mStatus.bssid, wifiInfo.getBSSID())) { return; } mStatus.signal = WifiManager.calculateSignalLevel(wifiInfo.getRssi(), WIFI_SIGNAL_LEVELS); } /** * Single step in state machine */ private void handleStateStep() { // Slog.v(WWS_TAG, "handleStateStep:: " + mStatus.state); switch (mStatus.state) { case DNS_FULL_CHECK: if (VDBG) { Slog.v(WWS_TAG, "DNS_FULL_CHECK: " + mDNSCheckLogStr); } long pingResponseTime = mDnsPinger.pingDns(mDnsPinger.getDns(), DNS_PING_TIMEOUT_MS); mStatus.dnsCheckTries++; if (pingResponseTime >= 0) mStatus.dnsCheckSuccesses++; if (DBG) { if (pingResponseTime >= 0) { mDNSCheckLogStr += " | " + pingResponseTime; } else { mDNSCheckLogStr += " | " + "x"; } } 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; } 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; } } Slog.d(WWS_TAG, "handleState::BlacklistedAP - Simiulating a new connection"); handleNewConnection(); break; } } private void doWalledGardenCheck() { if (!isWalledGardenTestEnabled()) { if (VDBG) Slog.v(WWS_TAG, "Skipping walled garden check - disabled"); mStatus.state = WatchdogState.CHECKS_COMPLETE; return; } 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."); } mStatus.state = WatchdogState.CHECKS_COMPLETE; return; } 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; } } 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; } /** * @return Delay in MS before next single DNS check can proceed. */ 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); } } /** * Helper to return wait time left given a min interval and last run * * @param interval minimum wait interval * @param lastTime last time action was performed in * SystemClock.elapsedRealtime() * @return non negative time to wait */ private static long waitTime(long interval, long lastTime) { long wait = interval + lastTime - SystemClock.elapsedRealtime(); return wait > 0 ? wait : 0; } 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; } /** * Checks the scan for new BBIDs using current mSsid */ private void updateBssids() { String curSsid = mStatus.ssid; HashSet bssids = mStatus.allBssids; List results = mWifiManager.getScanResults(); int oldNumBssids = bssids.size(); if (results == null) { if (VDBG) { Slog.v(WWS_TAG, "updateBssids: Got null scan results!"); } return; } for (ScanResult result : results) { if (result != null && curSsid.equals(result.SSID)) bssids.add(result.BSSID); } // 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)); // } } enum DnsCheckStatus { SUCCESS, FAILURE, INCOMPLETE } /** * Computes the current results of the dns check, ends early if outcome is * assured. */ private DnsCheckStatus currentDnsCheckStatus() { /** * After a full ping count, if we have more responses than this cutoff, * the outcome is success; else it is 'failure'. */ double pingResponseCutoff = MIN_RESPONSE_RATE * NUM_DNS_PINGS; int remainingChecks = NUM_DNS_PINGS - mStatus.dnsCheckTries; /** * Our final success count will be at least this big, so we're * guaranteed to succeed. */ if (mStatus.dnsCheckSuccesses >= pingResponseCutoff) { return DnsCheckStatus.SUCCESS; } /** * 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; } return DnsCheckStatus.INCOMPLETE; } 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; if (DBG) { mDNSCheckLogStr = String.format("Dns Check %d. Pinging %s on ssid [%s]: ", mStatus.numFullDNSchecks, mDnsPinger.getDns(), mStatus.ssid); } } /** * 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 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(); } } /** * 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 { /** * Major network event, object is NetworkInfo */ static final int MESSAGE_NETWORK_EVENT = 1; /** * Change in settings, no object */ static final int MESSAGE_CONTEXT_EVENT = 2; /** * Change in signal strength */ static final int RSSI_CHANGE_EVENT = 3; static final int SCAN_RESULTS_AVAILABLE = 4; static final int WIFI_STATE_CHANGE = 5; /** * 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; /** * @param looper */ public WifiWatchdogHandler(Looper looper) { super(looper); } boolean singleCheckQueued = false; long queuedSingleDnsCheckArrival; /** * 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); } boolean checkSequenceQueued = false; long queuedCheckSequenceArrival; /** * 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); } @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; } 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(); } 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; } } } 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.println(", bssids: " + mStatus.allBssids); pw.print(", hasCheckMessages? " + mHandler.hasMessages(WifiWatchdogHandler.CHECK_SEQUENCE_STEP)); pw.println(" hasSingleCheckMessages? " + mHandler.hasMessages(WifiWatchdogHandler.SINGLE_DNS_CHECK)); pw.println("DNS check log str: " + mDNSCheckLogStr); pw.println("lastSingleCheck: " + mStatus.lastSingleCheckTime); } /** * @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; } /** * @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; } /** * @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 ".*Google.*"; return pattern; } /** * @see android.provider.Settings.Secure#WIFI_WATCHDOG_ON */ private boolean isWatchdogEnabled() { return Settings.Secure.getInt(mContentResolver, Settings.Secure.WIFI_WATCHDOG_ON, 1) == 1; } }