Merge "Moved WifiWatchdogService and DnsPinger"
This commit is contained in:
committed by
Android (Google) Code Review
commit
6d815e9dd0
@@ -1,228 +0,0 @@
|
||||
/*
|
||||
* 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.Context;
|
||||
import android.net.ConnectivityManager;
|
||||
import android.net.LinkProperties;
|
||||
import android.net.NetworkUtils;
|
||||
import android.os.SystemClock;
|
||||
import android.provider.Settings;
|
||||
import android.util.Slog;
|
||||
|
||||
import java.net.DatagramPacket;
|
||||
import java.net.DatagramSocket;
|
||||
import java.net.InetAddress;
|
||||
import java.net.NetworkInterface;
|
||||
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. Socket does not bind to specified connection type
|
||||
* 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 = 32;
|
||||
|
||||
/** 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 Context mContext;
|
||||
private int mConnectionType;
|
||||
private InetAddress mDefaultDns;
|
||||
|
||||
private String TAG;
|
||||
|
||||
/**
|
||||
* @param connectionType The connection type from {@link ConnectivityManager}
|
||||
*/
|
||||
public DnsPinger(String TAG, Context context, int connectionType) {
|
||||
mContext = context;
|
||||
mConnectionType = connectionType;
|
||||
if (!ConnectivityManager.isNetworkTypeValid(connectionType)) {
|
||||
Slog.e(TAG, "Invalid connectionType in constructor: " + connectionType);
|
||||
}
|
||||
this.TAG = TAG;
|
||||
|
||||
mDefaultDns = getDefaultDns();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The first DNS in the link properties of the specified connection
|
||||
* type or the default system DNS if the link properties has null
|
||||
* dns set. Should not be null.
|
||||
*/
|
||||
public InetAddress getDns() {
|
||||
LinkProperties curLinkProps = getCurrentLinkProperties();
|
||||
if (curLinkProps == null) {
|
||||
Slog.e(TAG, "getCurLinkProperties:: LP for type" + mConnectionType + " is null!");
|
||||
return mDefaultDns;
|
||||
}
|
||||
|
||||
Collection<InetAddress> dnses = curLinkProps.getDnses();
|
||||
if (dnses == null || dnses.size() == 0) {
|
||||
Slog.v(TAG, "getDns::LinkProps has null dns - returning default");
|
||||
return mDefaultDns;
|
||||
}
|
||||
|
||||
return dnses.iterator().next();
|
||||
}
|
||||
|
||||
private LinkProperties getCurrentLinkProperties() {
|
||||
if (mConnectivityManager == null) {
|
||||
mConnectivityManager = (ConnectivityManager) mContext.getSystemService(
|
||||
Context.CONNECTIVITY_SERVICE);
|
||||
}
|
||||
|
||||
return mConnectivityManager.getLinkProperties(mConnectionType);
|
||||
}
|
||||
|
||||
private InetAddress getDefaultDns() {
|
||||
String dns = Settings.Secure.getString(mContext.getContentResolver(),
|
||||
Settings.Secure.DEFAULT_DNS_SERVER);
|
||||
if (dns == null || dns.length() == 0) {
|
||||
dns = mContext.getResources().getString(
|
||||
com.android.internal.R.string.config_default_dns_server);
|
||||
}
|
||||
try {
|
||||
return NetworkUtils.numericToInetAddress(dns);
|
||||
} catch (IllegalArgumentException e) {
|
||||
Slog.w(TAG, "getDefaultDns::malformed default dns address");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @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);
|
||||
|
||||
// Try to bind but continue ping if bind fails
|
||||
try {
|
||||
socket.setNetworkInterface(NetworkInterface.getByName(
|
||||
getCurrentLinkProperties().getInterfaceName()));
|
||||
} catch (Exception e) {
|
||||
Slog.d(TAG,"pingDns::Error binding to socket", e);
|
||||
}
|
||||
|
||||
byte[] buf = constructQuery();
|
||||
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @return google.com DNS query packet
|
||||
*/
|
||||
private static byte[] constructQuery() {
|
||||
byte[] buf = new byte[DNS_QUERY_BASE_SIZE];
|
||||
|
||||
// [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] = 0x01; // Recursion desired
|
||||
|
||||
// [4-5] bytes are for number of queries (QCOUNT)
|
||||
buf[5] = 0x01;
|
||||
|
||||
// [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-22] for google
|
||||
writeString(buf, 16, "google");
|
||||
|
||||
// [23-26] for com
|
||||
writeString(buf, 23, "com");
|
||||
|
||||
// [27] is a null byte terminator byte for the url
|
||||
|
||||
// [28-29] bytes are for QTYPE, set to 1 = A (host address)
|
||||
buf[29] = 0x01;
|
||||
|
||||
// [30-31] bytes are for QCLASS, set to 1 = IN (internet)
|
||||
buf[31] = 0x01;
|
||||
|
||||
return buf;
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the string's length and its contents to the buffer
|
||||
*/
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -36,6 +36,7 @@ import android.net.wifi.WifiManager;
|
||||
import android.net.wifi.WifiStateMachine;
|
||||
import android.net.wifi.WifiConfiguration;
|
||||
import android.net.wifi.WifiConfiguration.KeyMgmt;
|
||||
import android.net.wifi.WifiWatchdogService;
|
||||
import android.net.wifi.WpsConfiguration;
|
||||
import android.net.wifi.WpsResult;
|
||||
import android.net.ConnectivityManager;
|
||||
|
||||
@@ -1,768 +0,0 @@
|
||||
/*
|
||||
* 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}.
|
||||
* <p>
|
||||
* 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>
|
||||
* 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<String> allBssids = new HashSet<String>();
|
||||
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<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;
|
||||
}
|
||||
|
||||
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 "<title>.*Google.*</title>";
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user