diff options
author | Yuhao Zheng <yuhaozheng@google.com> | 2012-07-20 10:55:17 -0700 |
---|---|---|
committer | Irfan Sheriff <isheriff@google.com> | 2012-08-06 15:45:14 -0700 |
commit | b33227d23eb0ec3507192f94c2eee651a0f97783 (patch) | |
tree | f7b666696b5bf88e2e583ca8371c7bcadaf50501 /wifi | |
parent | 527d14dc3c2fd72f1cdfaaa7e249456778fe93e4 (diff) | |
download | frameworks_base-b33227d23eb0ec3507192f94c2eee651a0f97783.zip frameworks_base-b33227d23eb0ec3507192f94c2eee651a0f97783.tar.gz frameworks_base-b33227d23eb0ec3507192f94c2eee651a0f97783.tar.bz2 |
WifiWatchdog changes for poor link detection
- use packet loss (wlutil pktcnt) instead of frame loss, retune all parameters
- use wpa_supplicant to get packet loss counters, instead of netd
- handle BSSID roaming in all situations
- improve flapping avoidance mechanism by setting different target RSSI
- handle high packet loss in high RSSI (never seen in real testing)
- add more comments on how to set all parameters
Signed-off-by yuhaozheng@google.com
Change-Id: I33429f063d8625a458be4791edd83a86d5a723df
Diffstat (limited to 'wifi')
-rw-r--r-- | wifi/java/android/net/wifi/WifiNative.java | 8 | ||||
-rw-r--r-- | wifi/java/android/net/wifi/WifiStateMachine.java | 45 | ||||
-rw-r--r-- | wifi/java/android/net/wifi/WifiWatchdogStateMachine.java | 1116 |
3 files changed, 845 insertions, 324 deletions
diff --git a/wifi/java/android/net/wifi/WifiNative.java b/wifi/java/android/net/wifi/WifiNative.java index 4bf1ca3..84c565b 100644 --- a/wifi/java/android/net/wifi/WifiNative.java +++ b/wifi/java/android/net/wifi/WifiNative.java @@ -368,6 +368,14 @@ public class WifiNative { return doStringCommand("SIGNAL_POLL"); } + /** Example outout: + * TXGOOD=396 + * TXBAD=1 + */ + public String pktcntPoll() { + return doStringCommand("PKTCNT_POLL"); + } + public boolean startWpsPbc(String bssid) { if (TextUtils.isEmpty(bssid)) { return doBooleanCommand("WPS_PBC"); diff --git a/wifi/java/android/net/wifi/WifiStateMachine.java b/wifi/java/android/net/wifi/WifiStateMachine.java index 75bd6e2..c480759 100644 --- a/wifi/java/android/net/wifi/WifiStateMachine.java +++ b/wifi/java/android/net/wifi/WifiStateMachine.java @@ -51,6 +51,7 @@ import android.net.LinkProperties; import android.net.NetworkInfo; import android.net.NetworkInfo.DetailedState; import android.net.NetworkUtils; +import android.net.wifi.WifiWatchdogStateMachine.RssiPktcntStat; import android.net.wifi.WpsResult.Status; import android.net.wifi.p2p.WifiP2pManager; import android.net.wifi.p2p.WifiP2pService; @@ -1163,7 +1164,7 @@ public class WifiStateMachine extends StateMachine { case CMD_RSSI_POLL: case CMD_DELAYED_STOP_DRIVER: case WifiMonitor.SCAN_RESULTS_EVENT: - case WifiWatchdogStateMachine.RSSI_FETCH: + case WifiWatchdogStateMachine.RSSI_PKTCNT_FETCH: return false; default: return true; @@ -1514,6 +1515,30 @@ public class WifiStateMachine extends StateMachine { } } + /* + * Fetch TX packet counters on current connection + */ + private void fetchPktcntNative(RssiPktcntStat stat) { + String pktcntPoll = mWifiNative.pktcntPoll(); + + if (pktcntPoll != null) { + String[] lines = pktcntPoll.split("\n"); + for (String line : lines) { + String[] prop = line.split("="); + if (prop.length < 2) continue; + try { + if (prop[0].equals("TXGOOD")) { + stat.txgood = Integer.parseInt(prop[1]); + } else if (prop[0].equals("TXBAD")) { + stat.txbad = Integer.parseInt(prop[1]); + } + } catch (NumberFormatException e) { + //Ignore + } + } + } + } + private void configureLinkProperties() { if (mWifiConfigStore.isUsingStaticIp(mLastNetworkId)) { mLinkProperties = mWifiConfigStore.getLinkProperties(mLastNetworkId); @@ -1613,10 +1638,7 @@ public class WifiStateMachine extends StateMachine { mWifiInfo.setNetworkId(WifiConfiguration.INVALID_NETWORK_ID); } - if (state == SupplicantState.ASSOCIATING) { - /* BSSID is valid only in ASSOCIATING state */ - mWifiInfo.setBSSID(stateChangeResult.BSSID); - } + mWifiInfo.setBSSID(stateChangeResult.BSSID); mWifiInfo.setSSID(stateChangeResult.SSID); mSupplicantStateTracker.sendMessage(Message.obtain(message)); @@ -1925,8 +1947,8 @@ public class WifiStateMachine extends StateMachine { replyToMessage(message, WifiManager.DISABLE_NETWORK_FAILED, WifiManager.BUSY); break; - case WifiWatchdogStateMachine.RSSI_FETCH: - replyToMessage(message, WifiWatchdogStateMachine.RSSI_FETCH_FAILED); + case WifiWatchdogStateMachine.RSSI_PKTCNT_FETCH: + replyToMessage(message, WifiWatchdogStateMachine.RSSI_PKTCNT_FETCH_FAILED); break; default: loge("Error! unhandled message" + message); @@ -3129,10 +3151,13 @@ public class WifiStateMachine extends StateMachine { mRssiPollToken, 0), POLL_RSSI_INTERVAL_MSECS); } break; - case WifiWatchdogStateMachine.RSSI_FETCH: + case WifiWatchdogStateMachine.RSSI_PKTCNT_FETCH: + RssiPktcntStat stat = (RssiPktcntStat) message.obj; fetchRssiAndLinkSpeedNative(); - replyToMessage(message, WifiWatchdogStateMachine.RSSI_FETCH_SUCCEEDED, - mWifiInfo.getRssi()); + stat.rssi = mWifiInfo.getRssi(); + fetchPktcntNative(stat); + replyToMessage(message, WifiWatchdogStateMachine.RSSI_PKTCNT_FETCH_SUCCEEDED, + stat); break; default: return NOT_HANDLED; diff --git a/wifi/java/android/net/wifi/WifiWatchdogStateMachine.java b/wifi/java/android/net/wifi/WifiWatchdogStateMachine.java index 4018c74..7b4d113 100644 --- a/wifi/java/android/net/wifi/WifiWatchdogStateMachine.java +++ b/wifi/java/android/net/wifi/WifiWatchdogStateMachine.java @@ -26,19 +26,16 @@ import android.content.Intent; import android.content.IntentFilter; import android.content.res.Resources; import android.database.ContentObserver; -import android.net.arp.ArpPeer; import android.net.ConnectivityManager; -import android.net.LinkAddress; import android.net.LinkProperties; import android.net.NetworkInfo; -import android.net.RouteInfo; import android.net.Uri; import android.os.Message; import android.os.SystemClock; -import android.os.SystemProperties; import android.provider.Settings; import android.provider.Settings.Secure; import android.util.Log; +import android.util.LruCache; import com.android.internal.R; import com.android.internal.util.AsyncChannel; @@ -49,40 +46,36 @@ import com.android.internal.util.StateMachine; import java.io.IOException; import java.io.PrintWriter; import java.net.HttpURLConnection; -import java.net.InetAddress; -import java.net.SocketException; import java.net.URL; +import java.text.DecimalFormat; /** - * WifiWatchdogStateMachine monitors the connection to a Wi-Fi - * network. After the framework notifies that it has connected to an - * acccess point and is waiting for link to be verified, the watchdog - * takes over and verifies if the link is good by doing ARP pings to - * the gateway using {@link ArpPeer}. - * - * Upon successful verification, the watchdog notifies and continues - * to monitor the link afterwards when the RSSI level falls below - * a certain threshold. - - * When Wi-fi connects at L2 layer, the beacons from access point reach - * the device and it can maintain a connection, but the application - * connectivity can be flaky (due to bigger packet size exchange). - * - * We now monitor the quality of the last hop on - * Wi-Fi using signal strength and ARP connectivity as indicators - * to decide if the link is good enough to switch to Wi-Fi as the uplink. - * - * ARP pings are useful for link validation but can still get through - * when the application traffic fails to go through and are thus not - * the best indicator of real packet loss since they are tiny packets - * (28 bytes) and have a much low chance of packet corruption than the - * regular data packets. - * - * When signal strength and ARP are used together, it ends up working well in tests. - * The goal is to switch to Wi-Fi after validating ARP transfer - * and RSSI and then switching out of Wi-Fi when we hit a low - * signal strength threshold and then waiting until the signal strength - * improves and validating ARP transfer. + * WifiWatchdogStateMachine monitors the connection to a WiFi network. When WiFi + * connects at L2 layer, the beacons from access point reach the device and it + * can maintain a connection, but the application connectivity can be flaky (due + * to bigger packet size exchange). + * <p> + * We now monitor the quality of the last hop on WiFi using packet loss ratio as + * an indicator to decide if the link is good enough to switch to Wi-Fi as the + * uplink. + * <p> + * When WiFi is connected, the WiFi watchdog keeps sampling the RSSI and the + * instant packet loss, and record it as per-AP loss-to-rssi statistics. When + * the instant packet loss is higher than a threshold, the WiFi watchdog sends a + * poor link notification to avoid WiFi connection temporarily. + * <p> + * While WiFi is being avoided, the WiFi watchdog keep watching the RSSI to + * bring the WiFi connection back. Once the RSSI is high enough to achieve a + * lower packet loss, a good link detection is sent such that the WiFi + * connection become available again. + * <p> + * BSSID roaming has been taken into account. When user is moving across + * multiple APs, the WiFi watchdog will detect that and keep watching the + * currently connected AP. + * <p> + * Power impact should be minimal since much of the measurement relies on + * passive statistics already being tracked at the driver and the polling is + * done when screen is turned on and the RSSI is in a certain range. * * @hide */ @@ -91,92 +84,243 @@ public class WifiWatchdogStateMachine extends StateMachine { /* STOPSHIP: Keep this configurable for debugging until ship */ private static boolean DBG = false; private static final String TAG = "WifiWatchdogStateMachine"; - private static final String WALLED_GARDEN_NOTIFICATION_ID = "WifiWatchdog.walledgarden"; - /* RSSI Levels as used by notification icon - Level 4 -55 <= RSSI - Level 3 -66 <= RSSI < -55 - Level 2 -77 <= RSSI < -67 - Level 1 -88 <= RSSI < -78 - Level 0 RSSI < -88 */ - - /* Wi-fi connection is monitored actively below this - threshold */ - private static final int RSSI_LEVEL_MONITOR = 0; - /* Rssi threshold is at level 0 (-88dBm) */ - private static final int RSSI_MONITOR_THRESHOLD = -88; - /* Number of times RSSI is measured to be low before being avoided */ - private static final int RSSI_MONITOR_COUNT = 5; - private int mRssiMonitorCount = 0; - - /* Avoid flapping. The interval is changed over time as long as we continue to avoid - * under the max interval after which we reset the interval again */ - private static final int MIN_INTERVAL_AVOID_BSSID_MS[] = {0, 30 * 1000, 60 * 1000, - 5 * 60 * 1000, 30 * 60 * 1000}; - /* Index into the interval array MIN_INTERVAL_AVOID_BSSID_MS */ - private int mMinIntervalArrayIndex = 0; - - private long mLastBssidAvoidedTime; + private static final int BASE = Protocol.BASE_WIFI_WATCHDOG; - private int mCurrentSignalLevel; + /* Internal events */ + private static final int EVENT_WATCHDOG_TOGGLED = BASE + 1; + private static final int EVENT_NETWORK_STATE_CHANGE = BASE + 2; + private static final int EVENT_RSSI_CHANGE = BASE + 3; + private static final int EVENT_SUPPLICANT_STATE_CHANGE = BASE + 4; + private static final int EVENT_WIFI_RADIO_STATE_CHANGE = BASE + 5; + private static final int EVENT_WATCHDOG_SETTINGS_CHANGE = BASE + 6; + private static final int EVENT_BSSID_CHANGE = BASE + 7; + private static final int EVENT_SCREEN_ON = BASE + 8; + private static final int EVENT_SCREEN_OFF = BASE + 9; - private static final long DEFAULT_ARP_CHECK_INTERVAL_MS = 2 * 60 * 1000; - private static final long DEFAULT_RSSI_FETCH_INTERVAL_MS = 1000; - private static final long DEFAULT_WALLED_GARDEN_INTERVAL_MS = 30 * 60 * 1000; + /* Internal messages */ + private static final int CMD_DELAYED_WALLED_GARDEN_CHECK = BASE + 11; + private static final int CMD_RSSI_FETCH = BASE + 12; - private static final int DEFAULT_NUM_ARP_PINGS = 5; - private static final int DEFAULT_MIN_ARP_RESPONSES = 1; + /* Notifications from/to WifiStateMachine */ + static final int POOR_LINK_DETECTED = BASE + 21; + static final int GOOD_LINK_DETECTED = BASE + 22; + static final int RSSI_PKTCNT_FETCH = BASE + 23; + static final int RSSI_PKTCNT_FETCH_SUCCEEDED = BASE + 24; + static final int RSSI_PKTCNT_FETCH_FAILED = BASE + 25; + + /* + * RSSI levels as used by notification icon + * Level 4 -55 <= RSSI + * Level 3 -66 <= RSSI < -55 + * Level 2 -77 <= RSSI < -67 + * Level 1 -88 <= RSSI < -78 + * Level 0 RSSI < -88 + */ - private static final int DEFAULT_ARP_PING_TIMEOUT_MS = 100; + /** + * WiFi link statistics is monitored and recorded actively below this threshold. + * <p> + * Larger threshold is more adaptive but increases sampling cost. + */ + private static final int LINK_MONITOR_LEVEL_THRESHOLD = 4; - // See http://go/clientsdns for usage approval - private static final String DEFAULT_WALLED_GARDEN_URL = - "http://clients3.google.com/generate_204"; - private static final int WALLED_GARDEN_SOCKET_TIMEOUT_MS = 10000; + /** + * Remember packet loss statistics of how many BSSIDs. + * <p> + * Larger size is usually better but requires more space. + */ + private static final int BSSID_STAT_CACHE_SIZE = 20; - /* Some carrier apps might have support captive portal handling. Add some delay to allow - app authentication to be done before our test. - TODO: This should go away once we provide an API to apps to disable walled garden test - for certain SSIDs + /** + * RSSI range of a BSSID statistics. + * Within the range, (RSSI -> packet loss %) mappings are stored. + * <p> + * Larger range is usually better but requires more space. */ - private static final int WALLED_GARDEN_START_DELAY_MS = 3000; + private static final int BSSID_STAT_RANGE_LOW_DBM = -105; - private static final int BASE = Protocol.BASE_WIFI_WATCHDOG; + /** + * See {@link #BSSID_STAT_RANGE_LOW_DBM}. + */ + private static final int BSSID_STAT_RANGE_HIGH_DBM = -45; /** - * Indicates the enable setting of WWS may have changed + * How many consecutive empty data point to trigger a empty-cache detection. + * In this case, a preset/default loss value (function on RSSI) is used. + * <p> + * In normal uses, some RSSI values may never be seen due to channel randomness. + * However, the size of such empty RSSI chunk in normal use is generally 1~2. */ - private static final int EVENT_WATCHDOG_TOGGLED = BASE + 1; + private static final int BSSID_STAT_EMPTY_COUNT = 3; /** - * Indicates the wifi network state has changed. Passed w/ original intent - * which has a non-null networkInfo object + * Sample interval for packet loss statistics, in msec. + * <p> + * Smaller interval is more accurate but increases sampling cost (battery consumption). */ - private static final int EVENT_NETWORK_STATE_CHANGE = BASE + 2; - /* Passed with RSSI information */ - private static final int EVENT_RSSI_CHANGE = BASE + 3; - private static final int EVENT_WIFI_RADIO_STATE_CHANGE = BASE + 5; - private static final int EVENT_WATCHDOG_SETTINGS_CHANGE = BASE + 6; + private static final long LINK_SAMPLING_INTERVAL_MS = 1 * 1000; - /* Internal messages */ - private static final int CMD_ARP_CHECK = BASE + 11; - private static final int CMD_DELAYED_WALLED_GARDEN_CHECK = BASE + 12; - private static final int CMD_RSSI_FETCH = BASE + 13; + /** + * Coefficients (alpha) for moving average for packet loss tracking. + * Must be within (0.0, 1.0). + * <p> + * Equivalent number of samples: N = 2 / alpha - 1 . + * We want the historic loss to base on more data points to be statistically reliable. + * We want the current instant loss to base on less data points to be responsive. + */ + private static final double EXP_COEFFICIENT_RECORD = 0.1; - /* Notifications to WifiStateMachine */ - static final int POOR_LINK_DETECTED = BASE + 21; - static final int GOOD_LINK_DETECTED = BASE + 22; - static final int RSSI_FETCH = BASE + 23; - static final int RSSI_FETCH_SUCCEEDED = BASE + 24; - static final int RSSI_FETCH_FAILED = BASE + 25; + /** + * See {@link #EXP_COEFFICIENT_RECORD}. + */ + private static final double EXP_COEFFICIENT_MONITOR = 0.5; + + /** + * Thresholds for sending good/poor link notifications, in packet loss %. + * Good threshold must be smaller than poor threshold. + * Use smaller poor threshold to avoid WiFi more aggressively. + * Use smaller good threshold to bring back WiFi more conservatively. + * <p> + * When approaching the boundary, loss ratio jumps significantly within a few dBs. + * 50% loss threshold is a good balance between accuracy and reponsiveness. + * <=10% good threshold is a safe value to avoid jumping back to WiFi too easily. + */ + private static final double POOR_LINK_LOSS_THRESHOLD = 0.5; + + /** + * See {@link #POOR_LINK_LOSS_THRESHOLD}. + */ + private static final double GOOD_LINK_LOSS_THRESHOLD = 0.1; + + /** + * Number of samples to confirm before sending a poor link notification. + * Response time = confirm_count * sample_interval . + * <p> + * A smaller threshold improves response speed but may suffer from randomness. + * According to experiments, 3~5 are good values to achieve a balance. + * These parameters should be tuned along with {@link #LINK_SAMPLING_INTERVAL_MS}. + */ + private static final int POOR_LINK_SAMPLE_COUNT = 3; + + /** + * Minimum volume (converted from pkt/sec) to detect a poor link, to avoid randomness. + * <p> + * According to experiments, 1pkt/sec is too sensitive but 3pkt/sec is slightly unresponsive. + */ + private static final double POOR_LINK_MIN_VOLUME = 2.0 * LINK_SAMPLING_INTERVAL_MS / 1000.0; + + /** + * When a poor link is detected, we scan over this range (based on current + * poor link RSSI) for a target RSSI that satisfies a target packet loss. + * Refer to {@link #GOOD_LINK_TARGET}. + * <p> + * We want range_min not too small to avoid jumping back to WiFi too easily. + */ + private static final int GOOD_LINK_RSSI_RANGE_MIN = 3; + + /** + * See {@link #GOOD_LINK_RSSI_RANGE_MIN}. + */ + private static final int GOOD_LINK_RSSI_RANGE_MAX = 20; + + /** + * Adaptive good link target to avoid flapping. + * When a poor link is detected, a good link target is calculated as follows: + * <p> + * targetRSSI = min{ rssi | loss(rssi) < GOOD_LINK_LOSS_THRESHOLD } + rssi_adj[i], + * where rssi is in the above GOOD_LINK_RSSI_RANGE. + * targetCount = sample_count[i] . + * <p> + * While WiFi is being avoided, we keep monitoring its signal strength. + * Good link notification is sent when we see current RSSI >= targetRSSI + * for targetCount consecutive times. + * <p> + * Index i is incremented each time after a poor link detection. + * Index i is decreased to at most k if the last poor link was at lease reduce_time[k] ago. + * <p> + * Intuitively, larger index i makes it more difficult to get back to WiFi, avoiding flapping. + * In experiments, (+9 dB / 30 counts) makes it quite difficult to achieve. + * Avoid using it unless flapping is really bad (say, last poor link is only 1min ago). + */ + private static final GoodLinkTarget[] GOOD_LINK_TARGET = { + /* rssi_adj, sample_count, reduce_time */ + new GoodLinkTarget( 0, 3, 30 * 60000 ), + new GoodLinkTarget( 3, 5, 5 * 60000 ), + new GoodLinkTarget( 6, 10, 1 * 60000 ), + new GoodLinkTarget( 9, 30, 0 * 60000 ), + }; + + /** + * The max time to avoid a BSSID, to prevent avoiding forever. + * If current RSSI is at least min_rssi[i], the max avoidance time is at most max_time[i] + * <p> + * It is unusual to experience high packet loss at high RSSI. Something unusual must be + * happening (e.g. strong interference). For higher signal strengths, we set the avoidance + * time to be low to allow for quick turn around from temporary interference. + * <p> + * See {@link BssidStatistics#poorLinkDetected}. + */ + private static final MaxAvoidTime[] MAX_AVOID_TIME = { + /* max_time, min_rssi */ + new MaxAvoidTime( 30 * 60000, -200 ), + new MaxAvoidTime( 5 * 60000, -70 ), + new MaxAvoidTime( 0 * 60000, -55 ), + }; + + + private static final String WALLED_GARDEN_NOTIFICATION_ID = "WifiWatchdog.walledgarden"; + + private static final long DEFAULT_WALLED_GARDEN_INTERVAL_MS = 30 * 60 * 1000; + /** + * See http://go/clientsdns for usage approval + */ + private static final String DEFAULT_WALLED_GARDEN_URL = + "http://clients3.google.com/generate_204"; + private static final int WALLED_GARDEN_SOCKET_TIMEOUT_MS = 10000; + + /** + * Some carrier apps might have support captive portal handling. Add some + * delay to allow app authentication to be done before our test. TODO: This + * should go away once we provide an API to apps to disable walled garden + * test for certain SSIDs + */ + private static final int WALLED_GARDEN_START_DELAY_MS = 3000; + + + /* Framework related */ private Context mContext; private ContentResolver mContentResolver; private WifiManager mWifiManager; private IntentFilter mIntentFilter; private BroadcastReceiver mBroadcastReceiver; - private AsyncChannel mWsmChannel = new AsyncChannel();; + private AsyncChannel mWsmChannel = new AsyncChannel(); + private WifiInfo mWifiInfo; + private LinkProperties mLinkProperties; + + /* System settingss related */ + private static boolean sWifiOnly = false; + private boolean mPoorNetworkDetectionEnabled; + private long mWalledGardenIntervalMs; + private boolean mWalledGardenTestEnabled; + private String mWalledGardenUrl; + /* Wall garden detection related */ + private long mLastWalledGardenCheckTime = 0; + private boolean mWalledGardenNotificationShown; + + /* Poor link detection related */ + private LruCache<String, BssidStatistics> mBssidCache = + new LruCache<String, BssidStatistics>(BSSID_STAT_CACHE_SIZE); + private int mRssiFetchToken = 0; + private int mCurrentSignalLevel; + private BssidStatistics mCurrentBssid; + private VolumeWeightedEMA mCurrentLoss; + private boolean mIsScreenOn = true; + private static double sPresetLoss[]; + + /* WiFi watchdog state machine related */ private DefaultState mDefaultState = new DefaultState(); private WatchdogDisabledState mWatchdogDisabledState = new WatchdogDisabledState(); private WatchdogEnabledState mWatchdogEnabledState = new WatchdogEnabledState(); @@ -184,33 +328,10 @@ public class WifiWatchdogStateMachine extends StateMachine { private VerifyingLinkState mVerifyingLinkState = new VerifyingLinkState(); private ConnectedState mConnectedState = new ConnectedState(); private WalledGardenCheckState mWalledGardenCheckState = new WalledGardenCheckState(); - /* Online and watching link connectivity */ private OnlineWatchState mOnlineWatchState = new OnlineWatchState(); - /* RSSI level is below RSSI_LEVEL_MONITOR and needs close monitoring */ - private RssiMonitoringState mRssiMonitoringState = new RssiMonitoringState(); - /* Online and doing nothing */ + private LinkMonitoringState mLinkMonitoringState = new LinkMonitoringState(); private OnlineState mOnlineState = new OnlineState(); - private int mArpToken = 0; - private long mArpCheckIntervalMs; - private int mRssiFetchToken = 0; - private long mRssiFetchIntervalMs; - private long mWalledGardenIntervalMs; - private int mNumArpPings; - private int mMinArpResponses; - private int mArpPingTimeoutMs; - private boolean mPoorNetworkDetectionEnabled; - private boolean mWalledGardenTestEnabled; - private String mWalledGardenUrl; - - private WifiInfo mWifiInfo; - private LinkProperties mLinkProperties; - - private long mLastWalledGardenCheckTime = 0; - - private static boolean sWifiOnly = false; - private boolean mWalledGardenNotificationShown; - /** * STATE MAP * Default @@ -231,7 +352,7 @@ public class WifiWatchdogStateMachine extends StateMachine { setupNetworkReceiver(); - // The content observer to listen needs a handler + // the content observer to listen needs a handler registerForSettingsChanges(); registerForWatchdogToggle(); addState(mDefaultState); @@ -242,7 +363,7 @@ public class WifiWatchdogStateMachine extends StateMachine { addState(mConnectedState, mWatchdogEnabledState); addState(mWalledGardenCheckState, mConnectedState); addState(mOnlineWatchState, mConnectedState); - addState(mRssiMonitoringState, mOnlineWatchState); + addState(mLinkMonitoringState, mConnectedState); addState(mOnlineState, mConnectedState); if (isWatchdogEnabled()) { @@ -266,9 +387,9 @@ public class WifiWatchdogStateMachine extends StateMachine { // watchdog in an enabled state putSettingsBoolean(contentResolver, Settings.Secure.WIFI_WATCHDOG_ON, true); - // Disable poor network avoidance, but keep watchdog active for walled garden detection + // disable poor network avoidance, but keep watchdog active for walled garden detection if (sWifiOnly) { - log("Disabling poor network avoidance for wi-fi only device"); + logd("Disabling poor network avoidance for wi-fi only device"); putSettingsBoolean(contentResolver, Settings.Secure.WIFI_WATCHDOG_POOR_NETWORK_TEST_ENABLED, false); } @@ -283,15 +404,20 @@ public class WifiWatchdogStateMachine extends StateMachine { @Override public void onReceive(Context context, Intent intent) { String action = intent.getAction(); - if (action.equals(WifiManager.NETWORK_STATE_CHANGED_ACTION)) { - sendMessage(EVENT_NETWORK_STATE_CHANGE, intent); - } else if (action.equals(WifiManager.RSSI_CHANGED_ACTION)) { + if (action.equals(WifiManager.RSSI_CHANGED_ACTION)) { obtainMessage(EVENT_RSSI_CHANGE, intent.getIntExtra(WifiManager.EXTRA_NEW_RSSI, -200), 0).sendToTarget(); + } else if (action.equals(WifiManager.SUPPLICANT_STATE_CHANGED_ACTION)) { + sendMessage(EVENT_SUPPLICANT_STATE_CHANGE, intent); + } else if (action.equals(WifiManager.NETWORK_STATE_CHANGED_ACTION)) { + sendMessage(EVENT_NETWORK_STATE_CHANGE, intent); + } else if (action.equals(Intent.ACTION_SCREEN_ON)) { + sendMessage(EVENT_SCREEN_ON); + } else if (action.equals(Intent.ACTION_SCREEN_OFF)) { + sendMessage(EVENT_SCREEN_OFF); } else if (action.equals(WifiManager.WIFI_STATE_CHANGED_ACTION)) { - sendMessage(EVENT_WIFI_RADIO_STATE_CHANGE, - intent.getIntExtra(WifiManager.EXTRA_WIFI_STATE, - WifiManager.WIFI_STATE_UNKNOWN)); + sendMessage(EVENT_WIFI_RADIO_STATE_CHANGE,intent.getIntExtra( + WifiManager.EXTRA_WIFI_STATE, WifiManager.WIFI_STATE_UNKNOWN)); } } }; @@ -300,6 +426,9 @@ public class WifiWatchdogStateMachine extends StateMachine { mIntentFilter.addAction(WifiManager.NETWORK_STATE_CHANGED_ACTION); mIntentFilter.addAction(WifiManager.WIFI_STATE_CHANGED_ACTION); mIntentFilter.addAction(WifiManager.RSSI_CHANGED_ACTION); + mIntentFilter.addAction(WifiManager.SUPPLICANT_STATE_CHANGED_ACTION); + mIntentFilter.addAction(Intent.ACTION_SCREEN_ON); + mIntentFilter.addAction(Intent.ACTION_SCREEN_OFF); mContext.registerReceiver(mBroadcastReceiver, mIntentFilter); } @@ -331,22 +460,9 @@ public class WifiWatchdogStateMachine extends StateMachine { }; mContext.getContentResolver().registerContentObserver( - Settings.Secure.getUriFor( - Settings.Secure.WIFI_WATCHDOG_ARP_CHECK_INTERVAL_MS), - false, contentObserver); - mContext.getContentResolver().registerContentObserver( Settings.Secure.getUriFor(Settings.Secure.WIFI_WATCHDOG_WALLED_GARDEN_INTERVAL_MS), false, contentObserver); mContext.getContentResolver().registerContentObserver( - Settings.Secure.getUriFor(Settings.Secure.WIFI_WATCHDOG_NUM_ARP_PINGS), - false, contentObserver); - mContext.getContentResolver().registerContentObserver( - Settings.Secure.getUriFor(Settings.Secure.WIFI_WATCHDOG_MIN_ARP_RESPONSES), - false, contentObserver); - mContext.getContentResolver().registerContentObserver( - Settings.Secure.getUriFor(Settings.Secure.WIFI_WATCHDOG_ARP_PING_TIMEOUT_MS), - false, contentObserver); - mContext.getContentResolver().registerContentObserver( Settings.Secure.getUriFor(Settings.Secure.WIFI_WATCHDOG_POOR_NETWORK_TEST_ENABLED), false, contentObserver); mContext.getContentResolver().registerContentObserver( @@ -372,12 +488,10 @@ public class WifiWatchdogStateMachine extends StateMachine { urlConnection.setReadTimeout(WALLED_GARDEN_SOCKET_TIMEOUT_MS); urlConnection.setUseCaches(false); urlConnection.getInputStream(); - // We got a valid response, but not from the real google + // we got a valid response, but not from the real google return urlConnection.getResponseCode() != 204; } catch (IOException e) { - if (DBG) { - log("Walled garden check - probably not a portal: exception " + e); - } + if (DBG) logd("Walled garden check - probably not a portal: exception " + e); return false; } finally { if (urlConnection != null) { @@ -392,12 +506,7 @@ public class WifiWatchdogStateMachine extends StateMachine { pw.println("mWifiInfo: [" + mWifiInfo + "]"); pw.println("mLinkProperties: [" + mLinkProperties + "]"); pw.println("mCurrentSignalLevel: [" + mCurrentSignalLevel + "]"); - pw.println("mArpCheckIntervalMs: [" + mArpCheckIntervalMs+ "]"); - pw.println("mRssiFetchIntervalMs: [" + mRssiFetchIntervalMs + "]"); pw.println("mWalledGardenIntervalMs: [" + mWalledGardenIntervalMs + "]"); - pw.println("mNumArpPings: [" + mNumArpPings + "]"); - pw.println("mMinArpResponses: [" + mMinArpResponses + "]"); - pw.println("mArpPingTimeoutMs: [" + mArpPingTimeoutMs + "]"); pw.println("mPoorNetworkDetectionEnabled: [" + mPoorNetworkDetectionEnabled + "]"); pw.println("mWalledGardenTestEnabled: [" + mWalledGardenTestEnabled + "]"); pw.println("mWalledGardenUrl: [" + mWalledGardenUrl + "]"); @@ -405,28 +514,13 @@ public class WifiWatchdogStateMachine extends StateMachine { private boolean isWatchdogEnabled() { boolean ret = getSettingsBoolean(mContentResolver, Settings.Secure.WIFI_WATCHDOG_ON, true); - if (DBG) log("watchdog enabled " + ret); + if (DBG) logd("Watchdog enabled " + ret); return ret; } private void updateSettings() { - if (DBG) log("Updating secure settings"); - - mArpCheckIntervalMs = Secure.getLong(mContentResolver, - Secure.WIFI_WATCHDOG_ARP_CHECK_INTERVAL_MS, - DEFAULT_ARP_CHECK_INTERVAL_MS); - mRssiFetchIntervalMs = Secure.getLong(mContentResolver, - Secure.WIFI_WATCHDOG_RSSI_FETCH_INTERVAL_MS, - DEFAULT_RSSI_FETCH_INTERVAL_MS); - mNumArpPings = Secure.getInt(mContentResolver, - Secure.WIFI_WATCHDOG_NUM_ARP_PINGS, - DEFAULT_NUM_ARP_PINGS); - mMinArpResponses = Secure.getInt(mContentResolver, - Secure.WIFI_WATCHDOG_MIN_ARP_RESPONSES, - DEFAULT_MIN_ARP_RESPONSES); - mArpPingTimeoutMs = Secure.getInt(mContentResolver, - Secure.WIFI_WATCHDOG_ARP_PING_TIMEOUT_MS, - DEFAULT_ARP_PING_TIMEOUT_MS); + if (DBG) logd("Updating secure settings"); + mPoorNetworkDetectionEnabled = getSettingsBoolean(mContentResolver, Settings.Secure.WIFI_WATCHDOG_POOR_NETWORK_TEST_ENABLED, true); mWalledGardenTestEnabled = getSettingsBoolean(mContentResolver, @@ -440,7 +534,7 @@ public class WifiWatchdogStateMachine extends StateMachine { } private void setWalledGardenNotificationVisible(boolean visible) { - // If it should be hidden and it is already hidden, then noop + // if it should be hidden and it is already hidden, then noop if (!visible && !mWalledGardenNotificationShown) { return; } @@ -472,10 +566,13 @@ public class WifiWatchdogStateMachine extends StateMachine { mWalledGardenNotificationShown = visible; } + /** + * Default state, guard for unhandled messages. + */ class DefaultState extends State { @Override public void enter() { - if (DBG) log(getName() + "\n"); + if (DBG) logd(getName()); } @Override @@ -483,34 +580,42 @@ public class WifiWatchdogStateMachine extends StateMachine { switch (msg.what) { case EVENT_WATCHDOG_SETTINGS_CHANGE: updateSettings(); - if (DBG) { - log("Updating wifi-watchdog secure settings"); - } + if (DBG) logd("Updating wifi-watchdog secure settings"); break; case EVENT_RSSI_CHANGE: mCurrentSignalLevel = calculateSignalLevel(msg.arg1); break; case EVENT_WIFI_RADIO_STATE_CHANGE: case EVENT_NETWORK_STATE_CHANGE: - case CMD_ARP_CHECK: + case EVENT_SUPPLICANT_STATE_CHANGE: + case EVENT_BSSID_CHANGE: case CMD_DELAYED_WALLED_GARDEN_CHECK: case CMD_RSSI_FETCH: - case RSSI_FETCH_SUCCEEDED: - case RSSI_FETCH_FAILED: - //ignore + case RSSI_PKTCNT_FETCH_SUCCEEDED: + case RSSI_PKTCNT_FETCH_FAILED: + // ignore + break; + case EVENT_SCREEN_ON: + mIsScreenOn = true; + break; + case EVENT_SCREEN_OFF: + mIsScreenOn = false; break; default: - log("Unhandled message " + msg + " in state " + getCurrentState().getName()); + loge("Unhandled message " + msg + " in state " + getCurrentState().getName()); break; } return HANDLED; } } + /** + * WiFi watchdog is disabled by the setting. + */ class WatchdogDisabledState extends State { @Override public void enter() { - if (DBG) log(getName() + "\n"); + if (DBG) logd(getName()); } @Override @@ -527,8 +632,8 @@ public class WifiWatchdogStateMachine extends StateMachine { switch (networkInfo.getDetailedState()) { case VERIFYING_POOR_LINK: - if (DBG) log("Watchdog disabled, verify link"); - mWsmChannel.sendMessage(GOOD_LINK_DETECTED); + if (DBG) logd("Watchdog disabled, verify link"); + sendLinkStatusNotification(true); break; default: break; @@ -539,41 +644,46 @@ public class WifiWatchdogStateMachine extends StateMachine { } } + /** + * WiFi watchdog is enabled by the setting. + */ class WatchdogEnabledState extends State { @Override public void enter() { - if (DBG) log("WifiWatchdogService enabled"); + if (DBG) logd(getName()); } @Override public boolean processMessage(Message msg) { + Intent intent; switch (msg.what) { case EVENT_WATCHDOG_TOGGLED: if (!isWatchdogEnabled()) transitionTo(mWatchdogDisabledState); break; + case EVENT_NETWORK_STATE_CHANGE: - Intent intent = (Intent) msg.obj; - NetworkInfo networkInfo = (NetworkInfo) - intent.getParcelableExtra(WifiManager.EXTRA_NETWORK_INFO); + intent = (Intent) msg.obj; + NetworkInfo networkInfo = + (NetworkInfo) intent.getParcelableExtra(WifiManager.EXTRA_NETWORK_INFO); + if (DBG) logd("Network state change " + networkInfo.getDetailedState()); - if (DBG) log("network state change " + networkInfo.getDetailedState()); + mWifiInfo = (WifiInfo) intent.getParcelableExtra(WifiManager.EXTRA_WIFI_INFO); + updateCurrentBssid(mWifiInfo != null ? mWifiInfo.getBSSID() : null); switch (networkInfo.getDetailedState()) { case VERIFYING_POOR_LINK: mLinkProperties = (LinkProperties) intent.getParcelableExtra( WifiManager.EXTRA_LINK_PROPERTIES); - mWifiInfo = (WifiInfo) intent.getParcelableExtra( - WifiManager.EXTRA_WIFI_INFO); if (mPoorNetworkDetectionEnabled) { if (mWifiInfo == null) { - log("Ignoring link verification, mWifiInfo is NULL"); - mWsmChannel.sendMessage(GOOD_LINK_DETECTED); + if (DBG) logd("Ignoring link verification, mWifiInfo is NULL"); + sendLinkStatusNotification(true); } else { transitionTo(mVerifyingLinkState); } } else { - mWsmChannel.sendMessage(GOOD_LINK_DETECTED); + sendLinkStatusNotification(true); } break; case CONNECTED: @@ -588,12 +698,22 @@ public class WifiWatchdogStateMachine extends StateMachine { break; } break; + + case EVENT_SUPPLICANT_STATE_CHANGE: + intent = (Intent) msg.obj; + SupplicantState supplicantState = (SupplicantState) intent.getParcelableExtra( + WifiManager.EXTRA_NEW_STATE); + if (supplicantState == SupplicantState.COMPLETED) { + mWifiInfo = mWifiManager.getConnectionInfo(); + updateCurrentBssid(mWifiInfo.getBSSID()); + } + break; + case EVENT_WIFI_RADIO_STATE_CHANGE: - if ((Integer) msg.obj == WifiManager.WIFI_STATE_DISABLING) { - if (DBG) log("WifiStateDisabling -- Resetting WatchdogState"); + if ((Integer) msg.obj == WifiManager.WIFI_STATE_DISABLING) transitionTo(mNotConnectedState); - } break; + default: return NOT_HANDLED; } @@ -601,36 +721,31 @@ public class WifiWatchdogStateMachine extends StateMachine { setWalledGardenNotificationVisible(false); return HANDLED; } - - @Override - public void exit() { - if (DBG) log("WifiWatchdogService disabled"); - } } + /** + * WiFi is disconnected. + */ class NotConnectedState extends State { @Override public void enter() { - if (DBG) log(getName() + "\n"); + if (DBG) logd(getName()); } } + /** + * WiFi is connected, but waiting for good link detection message. + */ class VerifyingLinkState extends State { + + private int mSampleCount; + @Override public void enter() { - if (DBG) log(getName() + "\n"); - //Treat entry as an rssi change - handleRssiChange(); - } - - private void handleRssiChange() { - if (mCurrentSignalLevel <= RSSI_LEVEL_MONITOR) { - //stay here - if (DBG) log("enter VerifyingLinkState, stay level: " + mCurrentSignalLevel); - } else { - if (DBG) log("enter VerifyingLinkState, arp check level: " + mCurrentSignalLevel); - sendMessage(obtainMessage(CMD_ARP_CHECK, ++mArpToken, 0)); - } + if (DBG) logd(getName()); + mSampleCount = 0; + mCurrentBssid.newLinkDetected(); + sendMessage(obtainMessage(CMD_RSSI_FETCH, ++mRssiFetchToken, 0)); } @Override @@ -639,27 +754,51 @@ public class WifiWatchdogStateMachine extends StateMachine { case EVENT_WATCHDOG_SETTINGS_CHANGE: updateSettings(); if (!mPoorNetworkDetectionEnabled) { - mWsmChannel.sendMessage(GOOD_LINK_DETECTED); + sendLinkStatusNotification(true); } break; - case EVENT_RSSI_CHANGE: - mCurrentSignalLevel = calculateSignalLevel(msg.arg1); - handleRssiChange(); + + case EVENT_BSSID_CHANGE: + transitionTo(mVerifyingLinkState); break; - case CMD_ARP_CHECK: - if (msg.arg1 == mArpToken) { - boolean success = ArpPeer.doArp(mWifiInfo.getMacAddress(), mLinkProperties, - mArpPingTimeoutMs, mNumArpPings, mMinArpResponses); - if (success) { - if (DBG) log("Notify link is good " + mCurrentSignalLevel); - mWsmChannel.sendMessage(GOOD_LINK_DETECTED); + + case CMD_RSSI_FETCH: + if (msg.arg1 == mRssiFetchToken) { + mWsmChannel.sendMessage(RSSI_PKTCNT_FETCH, new RssiPktcntStat()); + sendMessageDelayed(obtainMessage(CMD_RSSI_FETCH, ++mRssiFetchToken, 0), + LINK_SAMPLING_INTERVAL_MS); + } + break; + + case RSSI_PKTCNT_FETCH_SUCCEEDED: + RssiPktcntStat stat = (RssiPktcntStat) msg.obj; + int rssi = stat.rssi; + if (DBG) logd("Fetch RSSI succeed, rssi=" + rssi); + + long time = mCurrentBssid.mBssidAvoidTimeMax - SystemClock.elapsedRealtime(); + if (time <= 0) { + // max avoidance time is met + if (DBG) logd("Max avoid time elapsed"); + sendLinkStatusNotification(true); + } else { + if (rssi >= mCurrentBssid.mGoodLinkTargetRssi) { + if (++mSampleCount >= mCurrentBssid.mGoodLinkTargetCount) { + // link is good again + if (DBG) logd("Good link detected, rssi=" + rssi); + mCurrentBssid.mBssidAvoidTimeMax = 0; + sendLinkStatusNotification(true); + } } else { - if (DBG) log("Continue ARP check, rssi level: " + mCurrentSignalLevel); - sendMessageDelayed(obtainMessage(CMD_ARP_CHECK, ++mArpToken, 0), - mArpCheckIntervalMs); + mSampleCount = 0; + if (DBG) logd("Link is still poor, time left=" + time); } } break; + + case RSSI_PKTCNT_FETCH_FAILED: + if (DBG) logd("RSSI_FETCH_FAILED"); + break; + default: return NOT_HANDLED; } @@ -667,19 +806,23 @@ public class WifiWatchdogStateMachine extends StateMachine { } } + /** + * WiFi is connected and link is verified. + */ class ConnectedState extends State { @Override public void enter() { - if (DBG) log(getName() + "\n"); + if (DBG) logd(getName()); } + @Override public boolean processMessage(Message msg) { switch (msg.what) { case EVENT_WATCHDOG_SETTINGS_CHANGE: updateSettings(); - //STOPSHIP: Remove this at ship + // STOPSHIP: Remove this at ship + logd("Updated secure settings and turned debug on"); DBG = true; - if (DBG) log("Updated secure settings and turned debug on"); if (mPoorNetworkDetectionEnabled) { transitionTo(mOnlineWatchState); @@ -692,11 +835,14 @@ public class WifiWatchdogStateMachine extends StateMachine { } } + /** + * Checking for wall garden. + */ class WalledGardenCheckState extends State { private int mWalledGardenToken = 0; @Override public void enter() { - if (DBG) log(getName() + "\n"); + if (DBG) logd(getName()); sendMessageDelayed(obtainMessage(CMD_DELAYED_WALLED_GARDEN_CHECK, ++mWalledGardenToken, 0), WALLED_GARDEN_START_DELAY_MS); } @@ -708,7 +854,7 @@ public class WifiWatchdogStateMachine extends StateMachine { if (msg.arg1 == mWalledGardenToken) { mLastWalledGardenCheckTime = SystemClock.elapsedRealtime(); if (isWalledGardenConnection()) { - if (DBG) log("Walled garden detected"); + if (DBG) logd("Walled garden detected"); setWalledGardenNotificationVisible(true); } transitionTo(mOnlineWatchState); @@ -721,11 +867,15 @@ public class WifiWatchdogStateMachine extends StateMachine { } } + /** + * RSSI is high enough and don't need link monitoring. + */ class OnlineWatchState extends State { + @Override public void enter() { - if (DBG) log(getName() + "\n"); + if (DBG) logd(getName()); if (mPoorNetworkDetectionEnabled) { - //Treat entry as an rssi change + // treat entry as an rssi change handleRssiChange(); } else { transitionTo(mOnlineState); @@ -733,10 +883,10 @@ public class WifiWatchdogStateMachine extends StateMachine { } private void handleRssiChange() { - if (mCurrentSignalLevel <= RSSI_LEVEL_MONITOR) { - transitionTo(mRssiMonitoringState); + if (mCurrentSignalLevel <= LINK_MONITOR_LEVEL_THRESHOLD) { + transitionTo(mLinkMonitoringState); } else { - //stay here + // stay here } } @@ -745,16 +895,7 @@ public class WifiWatchdogStateMachine extends StateMachine { switch (msg.what) { case EVENT_RSSI_CHANGE: mCurrentSignalLevel = calculateSignalLevel(msg.arg1); - //Ready to avoid bssid again ? - long time = android.os.SystemClock.elapsedRealtime(); - if (time - mLastBssidAvoidedTime > MIN_INTERVAL_AVOID_BSSID_MS[ - mMinIntervalArrayIndex]) { - handleRssiChange(); - } else { - if (DBG) log("Early to avoid " + mWifiInfo + " time: " + time + - " last avoided: " + mLastBssidAvoidedTime + - " mMinIntervalArrayIndex: " + mMinIntervalArrayIndex); - } + handleRssiChange(); break; default: return NOT_HANDLED; @@ -763,48 +904,110 @@ public class WifiWatchdogStateMachine extends StateMachine { } } - class RssiMonitoringState extends State { + /** + * Keep sampling the link and monitor any poor link situation. + */ + class LinkMonitoringState extends State { + + private int mSampleCount; + + private int mLastRssi; + private int mLastTxGood; + private int mLastTxBad; + + @Override public void enter() { - if (DBG) log(getName() + "\n"); + if (DBG) logd(getName()); + mSampleCount = 0; + mCurrentLoss = new VolumeWeightedEMA(EXP_COEFFICIENT_MONITOR); sendMessage(obtainMessage(CMD_RSSI_FETCH, ++mRssiFetchToken, 0)); } + @Override public boolean processMessage(Message msg) { switch (msg.what) { case EVENT_RSSI_CHANGE: mCurrentSignalLevel = calculateSignalLevel(msg.arg1); - if (mCurrentSignalLevel <= RSSI_LEVEL_MONITOR) { - //stay here; + if (mCurrentSignalLevel <= LINK_MONITOR_LEVEL_THRESHOLD) { + // stay here; } else { - //We dont need frequent RSSI monitoring any more + // we don't need frequent RSSI monitoring any more transitionTo(mOnlineWatchState); } break; + + case EVENT_BSSID_CHANGE: + transitionTo(mLinkMonitoringState); + break; + case CMD_RSSI_FETCH: - if (msg.arg1 == mRssiFetchToken) { - mWsmChannel.sendMessage(RSSI_FETCH); + if (!mIsScreenOn) { + transitionTo(mOnlineState); + } else if (msg.arg1 == mRssiFetchToken) { + mWsmChannel.sendMessage(RSSI_PKTCNT_FETCH, new RssiPktcntStat()); sendMessageDelayed(obtainMessage(CMD_RSSI_FETCH, ++mRssiFetchToken, 0), - mRssiFetchIntervalMs); + LINK_SAMPLING_INTERVAL_MS); } break; - case RSSI_FETCH_SUCCEEDED: - int rssi = msg.arg1; - if (DBG) log("RSSI_FETCH_SUCCEEDED: " + rssi); - if (msg.arg1 < RSSI_MONITOR_THRESHOLD) { - mRssiMonitorCount++; - } else { - mRssiMonitorCount = 0; - } - if (mRssiMonitorCount > RSSI_MONITOR_COUNT) { - sendPoorLinkDetected(); - ++mRssiFetchToken; + case RSSI_PKTCNT_FETCH_SUCCEEDED: + RssiPktcntStat stat = (RssiPktcntStat) msg.obj; + int rssi = stat.rssi; + int mrssi = (mLastRssi + rssi) / 2; + int txbad = stat.txbad; + int txgood = stat.txgood; + if (DBG) logd("Fetch RSSI succeed, rssi=" + rssi + " mrssi=" + mrssi + " txbad=" + + txbad + " txgood=" + txgood); + + // skip the first data point as we want incremental values + long now = SystemClock.elapsedRealtime(); + if (now - mCurrentBssid.mLastTimeSample < LINK_SAMPLING_INTERVAL_MS * 2) { + + // update packet loss statistics + int dbad = txbad - mLastTxBad; + int dgood = txgood - mLastTxGood; + int dtotal = dbad + dgood; + + if (dtotal > 0) { + // calculate packet loss in the last sampling interval + double loss = ((double) dbad) / ((double) dtotal); + + mCurrentLoss.update(loss, dtotal); + + if (DBG) { + DecimalFormat df = new DecimalFormat("#.##"); + logd("Incremental loss=" + dbad + "/" + dtotal + " Current loss=" + + df.format(mCurrentLoss.mValue * 100) + "% volume=" + + df.format(mCurrentLoss.mVolume)); + } + + mCurrentBssid.updateLoss(mrssi, loss, dtotal); + + // check for high packet loss and send poor link notification + if (mCurrentLoss.mValue > POOR_LINK_LOSS_THRESHOLD + && mCurrentLoss.mVolume > POOR_LINK_MIN_VOLUME) { + if (++mSampleCount >= POOR_LINK_SAMPLE_COUNT) + if (mCurrentBssid.poorLinkDetected(rssi)) { + sendLinkStatusNotification(false); + ++mRssiFetchToken; + } + } else { + mSampleCount = 0; + } + } } + + mCurrentBssid.mLastTimeSample = now; + mLastTxBad = txbad; + mLastTxGood = txgood; + mLastRssi = rssi; break; - case RSSI_FETCH_FAILED: - //can happen if we are waiting to get a disconnect notification - if (DBG) log("RSSI_FETCH_FAILED"); + + case RSSI_PKTCNT_FETCH_FAILED: + // can happen if we are waiting to get a disconnect notification + if (DBG) logd("RSSI_FETCH_FAILED"); break; + default: return NOT_HANDLED; } @@ -812,19 +1015,33 @@ public class WifiWatchdogStateMachine extends StateMachine { } } - /* Child state of ConnectedState indicating that we are online - * and there is nothing to do + /** + * Child state of ConnectedState indicating that we are online and there is nothing to do. */ class OnlineState extends State { @Override public void enter() { - if (DBG) log(getName() + "\n"); + if (DBG) logd(getName()); + } + + @Override + public boolean processMessage(Message msg) { + switch (msg.what) { + case EVENT_SCREEN_ON: + mIsScreenOn = true; + if (mPoorNetworkDetectionEnabled) + transitionTo(mOnlineWatchState); + break; + default: + return NOT_HANDLED; + } + return HANDLED; } } private boolean shouldCheckWalledGarden() { if (!mWalledGardenTestEnabled) { - if (DBG) log("Skipping walled garden check - disabled"); + if (DBG) logd("Skipping walled garden check - disabled"); return false; } @@ -832,40 +1049,57 @@ public class WifiWatchdogStateMachine extends StateMachine { - SystemClock.elapsedRealtime(); if (mLastWalledGardenCheckTime != 0 && waitTime > 0) { - if (DBG) { - log("Skipping walled garden check - wait " + - waitTime + " ms."); - } + if (DBG) logd("Skipping walled garden check - wait " + waitTime + " ms."); return false; } return true; } + private void updateCurrentBssid(String bssid) { + if (DBG) logd("Update current BSSID to " + (bssid != null ? bssid : "null")); + + // if currently not connected, then set current BSSID to null + if (bssid == null) { + if (mCurrentBssid == null) return; + mCurrentBssid = null; + if (DBG) logd("BSSID changed"); + sendMessage(EVENT_BSSID_CHANGE); + return; + } + + // if it is already the current BSSID, then done + if (mCurrentBssid != null && bssid.equals(mCurrentBssid.mBssid)) return; + + // search for the new BSSID in the cache, add to cache if not found + mCurrentBssid = mBssidCache.get(bssid); + if (mCurrentBssid == null) { + mCurrentBssid = new BssidStatistics(bssid); + mBssidCache.put(bssid, mCurrentBssid); + } + + // send BSSID change notification + if (DBG) logd("BSSID changed"); + sendMessage(EVENT_BSSID_CHANGE); + } + private int calculateSignalLevel(int rssi) { - int signalLevel = WifiManager.calculateSignalLevel(rssi, - WifiManager.RSSI_LEVELS); - if (DBG) log("RSSI current: " + mCurrentSignalLevel + "new: " + rssi + ", " + signalLevel); + int signalLevel = WifiManager.calculateSignalLevel(rssi, WifiManager.RSSI_LEVELS); + if (DBG) + logd("RSSI current: " + mCurrentSignalLevel + " new: " + rssi + ", " + signalLevel); return signalLevel; } - private void sendPoorLinkDetected() { - if (DBG) log("send POOR_LINK_DETECTED " + mWifiInfo); - mWsmChannel.sendMessage(POOR_LINK_DETECTED); - - long time = android.os.SystemClock.elapsedRealtime(); - if (time - mLastBssidAvoidedTime > MIN_INTERVAL_AVOID_BSSID_MS[ - MIN_INTERVAL_AVOID_BSSID_MS.length - 1]) { - mMinIntervalArrayIndex = 1; - if (DBG) log("set mMinIntervalArrayIndex to 1"); + private void sendLinkStatusNotification(boolean isGood) { + if (DBG) logd("########################################"); + if (isGood) { + mWsmChannel.sendMessage(GOOD_LINK_DETECTED); + mCurrentBssid.mLastTimeGood = SystemClock.elapsedRealtime(); + logd("Good link notification is sent"); } else { - - if (mMinIntervalArrayIndex < MIN_INTERVAL_AVOID_BSSID_MS.length - 1) { - mMinIntervalArrayIndex++; - } - if (DBG) log("mMinIntervalArrayIndex: " + mMinIntervalArrayIndex); + mWsmChannel.sendMessage(POOR_LINK_DETECTED); + mCurrentBssid.mLastTimePoor = SystemClock.elapsedRealtime(); + logd("Poor link notification is sent"); } - - mLastBssidAvoidedTime = android.os.SystemClock.elapsedRealtime(); } /** @@ -884,30 +1118,28 @@ public class WifiWatchdogStateMachine extends StateMachine { } /** - * Convenience function for retrieving a single secure settings value - * as a boolean. Note that internally setting values are always - * stored as strings; this function converts the string to a boolean - * for you. The default value will be returned if the setting is - * not defined or not a valid boolean. + * Convenience function for retrieving a single secure settings value as a + * boolean. Note that internally setting values are always stored as + * strings; this function converts the string to a boolean for you. The + * default value will be returned if the setting is not defined or not a + * valid boolean. * * @param cr The ContentResolver to access. * @param name The name of the setting to retrieve. * @param def Value to return if the setting is not defined. - * - * @return The setting's current value, or 'def' if it is not defined - * or not a valid boolean. + * @return The setting's current value, or 'def' if it is not defined or not + * a valid boolean. */ private static boolean getSettingsBoolean(ContentResolver cr, String name, boolean def) { return Settings.Secure.getInt(cr, name, def ? 1 : 0) == 1; } /** - * Convenience function for updating a single settings value as an - * integer. This will either create a new entry in the table if the - * given name does not exist, or modify the value of the existing row - * with that name. Note that internally setting values are always - * stored as strings, so this function converts the given value to a - * string before storing it. + * Convenience function for updating a single settings value as an integer. + * This will either create a new entry in the table if the given name does + * not exist, or modify the value of the existing row with that name. Note + * that internally setting values are always stored as strings, so this + * function converts the given value to a string before storing it. * * @param cr The ContentResolver to access. * @param name The name of the setting to modify. @@ -918,11 +1150,267 @@ public class WifiWatchdogStateMachine extends StateMachine { return Settings.Secure.putInt(cr, name, value ? 1 : 0); } - private static void log(String s) { + private static void logd(String s) { Log.d(TAG, s); } private static void loge(String s) { Log.e(TAG, s); } + + /** + * Bundle of RSSI and packet count information + */ + public class RssiPktcntStat { + public int rssi; + public int txgood; + public int txbad; + } + + /** + * Bundle of good link count parameters + */ + private static class GoodLinkTarget { + public final int RSSI_ADJ_DBM; + public final int SAMPLE_COUNT; + public final int REDUCE_TIME_MS; + public GoodLinkTarget(int adj, int count, int time) { + RSSI_ADJ_DBM = adj; + SAMPLE_COUNT = count; + REDUCE_TIME_MS = time; + } + } + + /** + * Bundle of max avoidance time parameters + */ + private static class MaxAvoidTime { + public final int TIME_MS; + public final int MIN_RSSI_DBM; + public MaxAvoidTime(int time, int rssi) { + TIME_MS = time; + MIN_RSSI_DBM = rssi; + } + } + + /** + * Volume-weighted Exponential Moving Average (V-EMA) + * - volume-weighted: each update has its own weight (number of packets) + * - exponential: O(1) time and O(1) space for both update and query + * - moving average: reflect most recent results and expire old ones + */ + private class VolumeWeightedEMA { + private double mValue; + private double mVolume; + private double mProduct; + private final double mAlpha; + + public VolumeWeightedEMA(double coefficient) { + mValue = 0.0; + mVolume = 0.0; + mProduct = 0.0; + mAlpha = coefficient; + } + + public void update(double newValue, int newVolume) { + if (newVolume <= 0) return; + // core update formulas + double newProduct = newValue * newVolume; + mProduct = mAlpha * newProduct + (1 - mAlpha) * mProduct; + mVolume = mAlpha * newVolume + (1 - mAlpha) * mVolume; + mValue = mProduct / mVolume; + } + } + + /** + * Record (RSSI -> pakce loss %) mappings of one BSSID + */ + private class BssidStatistics { + + /* MAC address of this BSSID */ + private final String mBssid; + + /* RSSI -> packet loss % mappings */ + private VolumeWeightedEMA[] mEntries; + private int mRssiBase; + private int mEntriesSize; + + /* Target to send good link notification, set when poor link is detected */ + private int mGoodLinkTargetRssi; + private int mGoodLinkTargetCount; + + /* Index of GOOD_LINK_TARGET array */ + private int mGoodLinkTargetIndex; + + /* Timestamps of some last events */ + private long mLastTimeSample; + private long mLastTimeGood; + private long mLastTimePoor; + + /* Max time to avoid this BSSID */ + private long mBssidAvoidTimeMax; + + /** + * Constructor + * + * @param bssid is the address of this BSSID + */ + public BssidStatistics(String bssid) { + this.mBssid = bssid; + mRssiBase = BSSID_STAT_RANGE_LOW_DBM; + mEntriesSize = BSSID_STAT_RANGE_HIGH_DBM - BSSID_STAT_RANGE_LOW_DBM + 1; + mEntries = new VolumeWeightedEMA[mEntriesSize]; + for (int i = 0; i < mEntriesSize; i++) + mEntries[i] = new VolumeWeightedEMA(EXP_COEFFICIENT_RECORD); + } + + /** + * Update this BSSID cache + * + * @param rssi is the RSSI + * @param value is the new instant loss value at this RSSI + * @param volume is the volume for this single update + */ + public void updateLoss(int rssi, double value, int volume) { + if (volume <= 0) return; + int index = rssi - mRssiBase; + if (index < 0 || index >= mEntriesSize) return; + mEntries[index].update(value, volume); + if (DBG) { + DecimalFormat df = new DecimalFormat("#.##"); + logd("Cache updated: loss[" + rssi + "]=" + df.format(mEntries[index].mValue * 100) + + "% volume=" + df.format(mEntries[index].mVolume)); + } + } + + /** + * Get preset loss if the cache has insufficient data, observed from experiments. + * + * @param rssi is the input RSSI + * @return preset loss of the given RSSI + */ + public double presetLoss(int rssi) { + if (rssi <= -90) return 1.0; + if (rssi > 0) return 0.0; + + if (sPresetLoss == null) { + // pre-calculate all preset losses only once, then reuse them + final int size = 90; + sPresetLoss = new double[size]; + for (int i = 0; i < size; i++) sPresetLoss[i] = 1.0 / Math.pow(90 - i, 1.5); + } + return sPresetLoss[-rssi]; + } + + /** + * A poor link is detected, calculate a target RSSI to bring WiFi back. + * + * @param rssi is the current RSSI + * @return true iff the current BSSID should be avoided + */ + public boolean poorLinkDetected(int rssi) { + if (DBG) logd("Poor link detected, rssi=" + rssi); + + long now = SystemClock.elapsedRealtime(); + long lastGood = now - mLastTimeGood; + long lastPoor = now - mLastTimePoor; + + // reduce the difficulty of good link target if last avoidance was long time ago + while (mGoodLinkTargetIndex > 0 + && lastPoor >= GOOD_LINK_TARGET[mGoodLinkTargetIndex - 1].REDUCE_TIME_MS) + mGoodLinkTargetIndex--; + mGoodLinkTargetCount = GOOD_LINK_TARGET[mGoodLinkTargetIndex].SAMPLE_COUNT; + + // scan for a target RSSI at which the link is good + int from = rssi + GOOD_LINK_RSSI_RANGE_MIN; + int to = rssi + GOOD_LINK_RSSI_RANGE_MAX; + mGoodLinkTargetRssi = findRssiTarget(from, to, GOOD_LINK_LOSS_THRESHOLD); + mGoodLinkTargetRssi += GOOD_LINK_TARGET[mGoodLinkTargetIndex].RSSI_ADJ_DBM; + if (mGoodLinkTargetIndex < GOOD_LINK_TARGET.length - 1) mGoodLinkTargetIndex++; + + // calculate max avoidance time to prevent avoiding forever + int p = 0, pmax = MAX_AVOID_TIME.length - 1; + while (p < pmax && rssi >= MAX_AVOID_TIME[p + 1].MIN_RSSI_DBM) p++; + long avoidMax = MAX_AVOID_TIME[p].TIME_MS; + + // don't avoid if max avoidance time is 0 (RSSI is super high) + if (avoidMax <= 0) return false; + + // set max avoidance time, send poor link notification + mBssidAvoidTimeMax = now + avoidMax; + + if (DBG) logd("goodRssi=" + mGoodLinkTargetRssi + " goodCount=" + mGoodLinkTargetCount + + " lastGood=" + lastGood + " lastPoor=" + lastPoor + " avoidMax=" + avoidMax); + + return true; + } + + /** + * A new BSSID is connected, recalculate target RSSI threshold + */ + public void newLinkDetected() { + // if this BSSID is currently being avoided, the reuse those values + if (mBssidAvoidTimeMax > 0) { + if (DBG) logd("Previous avoidance still in effect, rssi=" + mGoodLinkTargetRssi + + " count=" + mGoodLinkTargetCount); + return; + } + + // calculate a new RSSI threshold for new link verifying + int from = BSSID_STAT_RANGE_LOW_DBM; + int to = BSSID_STAT_RANGE_HIGH_DBM; + mGoodLinkTargetRssi = findRssiTarget(from, to, GOOD_LINK_LOSS_THRESHOLD); + mGoodLinkTargetCount = 1; + mBssidAvoidTimeMax = SystemClock.elapsedRealtime() + MAX_AVOID_TIME[0].TIME_MS; + if (DBG) logd("New link verifying target set, rssi=" + mGoodLinkTargetRssi + " count=" + + mGoodLinkTargetCount); + } + + /** + * Return the first RSSI within the range where loss[rssi] < threshold + * + * @param from start scanning from this RSSI + * @param to stop scanning at this RSSI + * @param threshold target threshold for scanning + * @return target RSSI + */ + public int findRssiTarget(int from, int to, double threshold) { + from -= mRssiBase; + to -= mRssiBase; + int emptyCount = 0; + int d = from < to ? 1 : -1; + for (int i = from; i != to; i += d) + // don't use a data point if it volume is too small (statistically unreliable) + if (i >= 0 && i < mEntriesSize && mEntries[i].mVolume > 1.0) { + emptyCount = 0; + if (mEntries[i].mValue < threshold) { + // scan target found + int rssi = mRssiBase + i; + if (DBG) { + DecimalFormat df = new DecimalFormat("#.##"); + logd("Scan target found: rssi=" + rssi + " threshold=" + + df.format(threshold * 100) + "% value=" + + df.format(mEntries[i].mValue * 100) + "% volume=" + + df.format(mEntries[i].mVolume)); + } + return rssi; + } + } else if (++emptyCount >= BSSID_STAT_EMPTY_COUNT) { + // cache has insufficient data around this RSSI, use preset loss instead + int rssi = mRssiBase + i; + double lossPreset = presetLoss(rssi); + if (lossPreset < threshold) { + if (DBG) { + DecimalFormat df = new DecimalFormat("#.##"); + logd("Scan target found: rssi=" + rssi + " threshold=" + + df.format(threshold * 100) + "% value=" + + df.format(lossPreset * 100) + "% volume=preset"); + } + return rssi; + } + } + + return mRssiBase + to; + } + } } |