/* * 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.LinkProperties; import android.net.NetworkInfo; import android.net.wifi.ScanResult; import android.net.wifi.WifiInfo; import android.net.wifi.WifiManager; import android.net.Uri; import android.os.Handler; import android.os.Looper; import android.os.Message; 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.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.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. *
* The watchdog has a few safeguards: *
* 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). *
* 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; private static final boolean D = true; 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; /** * 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; 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"); } } /** * 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_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 "
* 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; /** Check whether the connection is a walled garden */ static final int ACTION_CHECK_WALLED_GARDEN = 4; /** * 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. *
* 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; /* 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)); } 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) { 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; } } /** * 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(); } } 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; 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); } } } /** * Receives Wi-Fi broadcasts. *
* 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) { onDisconnected(); } else if (wifiState == WifiManager.WIFI_STATE_ENABLED) { onEnabled(); } } }; /** * Describes an access point by its SSID and BSSID. * */ private static class AccessPoint { String ssid; String bssid; /** * @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 + ")"; } } /** * 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. *
* 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(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; } 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) { 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); } } } }