WifiWatchdogStateMachinepublic class WifiWatchdogStateMachine extends com.android.internal.util.StateMachine 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).
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.
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.
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.
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.
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. |
Fields Summary |
---|
private static final boolean | DBG | private static final int | BASE | private static final int | EVENT_WATCHDOG_TOGGLED | private static final int | EVENT_NETWORK_STATE_CHANGE | private static final int | EVENT_RSSI_CHANGE | private static final int | EVENT_SUPPLICANT_STATE_CHANGE | private static final int | EVENT_WIFI_RADIO_STATE_CHANGE | private static final int | EVENT_WATCHDOG_SETTINGS_CHANGE | private static final int | EVENT_BSSID_CHANGE | private static final int | EVENT_SCREEN_ON | private static final int | EVENT_SCREEN_OFF | private static final int | CMD_RSSI_FETCH | static final int | POOR_LINK_DETECTED | static final int | GOOD_LINK_DETECTED | private static final int | LINK_MONITOR_LEVEL_THRESHOLDWiFi link statistics is monitored and recorded actively below this threshold.
Larger threshold is more adaptive but increases sampling cost. | private static final int | BSSID_STAT_CACHE_SIZERemember packet loss statistics of how many BSSIDs.
Larger size is usually better but requires more space. | private static final int | BSSID_STAT_RANGE_LOW_DBMRSSI range of a BSSID statistics.
Within the range, (RSSI -> packet loss %) mappings are stored.
Larger range is usually better but requires more space. | private static final int | BSSID_STAT_RANGE_HIGH_DBMSee {@link #BSSID_STAT_RANGE_LOW_DBM}. | private static final int | BSSID_STAT_EMPTY_COUNTHow many consecutive empty data point to trigger a empty-cache detection.
In this case, a preset/default loss value (function on RSSI) is used.
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 long | LINK_SAMPLING_INTERVAL_MSSample interval for packet loss statistics, in msec.
Smaller interval is more accurate but increases sampling cost (battery consumption). | private static final double | EXP_COEFFICIENT_RECORDCoefficients (alpha) for moving average for packet loss tracking.
Must be within (0.0, 1.0).
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_MONITORSee {@link #EXP_COEFFICIENT_RECORD}. | private static final double | POOR_LINK_LOSS_THRESHOLDThresholds 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.
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 | GOOD_LINK_LOSS_THRESHOLDSee {@link #POOR_LINK_LOSS_THRESHOLD}. | private static final int | POOR_LINK_SAMPLE_COUNTNumber of samples to confirm before sending a poor link notification.
Response time = confirm_count * sample_interval .
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 double | POOR_LINK_MIN_VOLUMEMinimum volume (converted from pkt/sec) to detect a poor link, to avoid randomness.
According to experiments, 1pkt/sec is too sensitive but 3pkt/sec is slightly unresponsive. | private static final int | GOOD_LINK_RSSI_RANGE_MINWhen 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}.
We want range_min not too small to avoid jumping back to WiFi too easily. | private static final int | GOOD_LINK_RSSI_RANGE_MAXSee {@link #GOOD_LINK_RSSI_RANGE_MIN}. | private static final GoodLinkTarget[] | GOOD_LINK_TARGETAdaptive good link target to avoid flapping.
When a poor link is detected, a good link target is calculated as follows:
targetRSSI = min { rssi | loss(rssi) < GOOD_LINK_LOSS_THRESHOLD } + rssi_adj[i],
where rssi is within the above GOOD_LINK_RSSI_RANGE.
targetCount = sample_count[i] .
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.
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.
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 < 1 min ago). | private static final MaxAvoidTime[] | MAX_AVOID_TIMEThe 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]
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.
See {@link BssidStatistics#poorLinkDetected}. | private android.content.Context | mContext | private android.content.ContentResolver | mContentResolver | private android.net.wifi.WifiManager | mWifiManager | private android.content.IntentFilter | mIntentFilter | private android.content.BroadcastReceiver | mBroadcastReceiver | private com.android.internal.util.AsyncChannel | mWsmChannel | private android.net.wifi.WifiInfo | mWifiInfo | private android.net.LinkProperties | mLinkProperties | private static boolean | sWifiOnly | private boolean | mPoorNetworkDetectionEnabled | private android.util.LruCache | mBssidCache | private int | mRssiFetchToken | private int | mCurrentSignalLevel | private BssidStatistics | mCurrentBssid | private VolumeWeightedEMA | mCurrentLoss | private boolean | mIsScreenOn | private static double[] | sPresetLoss | private DefaultState | mDefaultState | private WatchdogDisabledState | mWatchdogDisabledState | private WatchdogEnabledState | mWatchdogEnabledState | private NotConnectedState | mNotConnectedState | private VerifyingLinkState | mVerifyingLinkState | private ConnectedState | mConnectedState | private OnlineWatchState | mOnlineWatchState | private LinkMonitoringState | mLinkMonitoringState | private OnlineState | mOnlineState |
Constructors Summary |
---|
private WifiWatchdogStateMachine(android.content.Context context, android.os.Messenger dstMessenger)STATE MAP
Default
/ \
Disabled Enabled
/ \ \
NotConnected Verifying Connected
/---------\
(all other states)
super("WifiWatchdogStateMachine");
mContext = context;
mContentResolver = context.getContentResolver();
mWifiManager = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
mWsmChannel.connectSync(mContext, getHandler(), dstMessenger);
setupNetworkReceiver();
// the content observer to listen needs a handler
registerForSettingsChanges();
registerForWatchdogToggle();
addState(mDefaultState);
addState(mWatchdogDisabledState, mDefaultState);
addState(mWatchdogEnabledState, mDefaultState);
addState(mNotConnectedState, mWatchdogEnabledState);
addState(mVerifyingLinkState, mWatchdogEnabledState);
addState(mConnectedState, mWatchdogEnabledState);
addState(mOnlineWatchState, mConnectedState);
addState(mLinkMonitoringState, mConnectedState);
addState(mOnlineState, mConnectedState);
if (isWatchdogEnabled()) {
setInitialState(mNotConnectedState);
} else {
setInitialState(mWatchdogDisabledState);
}
setLogRecSize(25);
setLogOnlyTransitions(true);
updateSettings();
|
Methods Summary |
---|
private int | calculateSignalLevel(int rssi)
int signalLevel = WifiManager.calculateSignalLevel(rssi, WifiManager.RSSI_LEVELS);
if (DBG)
logd("RSSI current: " + mCurrentSignalLevel + " new: " + rssi + ", " + signalLevel);
return signalLevel;
| public void | dump(java.io.FileDescriptor fd, java.io.PrintWriter pw, java.lang.String[] args)
super.dump(fd, pw, args);
pw.println("mWifiInfo: [" + mWifiInfo + "]");
pw.println("mLinkProperties: [" + mLinkProperties + "]");
pw.println("mCurrentSignalLevel: [" + mCurrentSignalLevel + "]");
pw.println("mPoorNetworkDetectionEnabled: [" + mPoorNetworkDetectionEnabled + "]");
| private static boolean | getSettingsGlobalBoolean(android.content.ContentResolver cr, java.lang.String name, boolean def)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.
return Settings.Global.getInt(cr, name, def ? 1 : 0) == 1;
| private boolean | isWatchdogEnabled()
boolean ret = getSettingsGlobalBoolean(
mContentResolver, Settings.Global.WIFI_WATCHDOG_ON, true);
if (DBG) logd("Watchdog enabled " + ret);
return ret;
| public static com.android.server.wifi.WifiWatchdogStateMachine | makeWifiWatchdogStateMachine(android.content.Context context, android.os.Messenger dstMessenger)
ContentResolver contentResolver = context.getContentResolver();
ConnectivityManager cm = (ConnectivityManager) context.getSystemService(
Context.CONNECTIVITY_SERVICE);
sWifiOnly = (cm.isNetworkSupported(ConnectivityManager.TYPE_MOBILE) == false);
// Watchdog is always enabled. Poor network detection can be seperately turned on/off
// TODO: Remove this setting & clean up state machine since we always have
// watchdog in an enabled state
putSettingsGlobalBoolean(contentResolver, Settings.Global.WIFI_WATCHDOG_ON, true);
WifiWatchdogStateMachine wwsm = new WifiWatchdogStateMachine(context, dstMessenger);
wwsm.start();
return wwsm;
| private static boolean | putSettingsGlobalBoolean(android.content.ContentResolver cr, java.lang.String name, boolean value)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.
return Settings.Global.putInt(cr, name, value ? 1 : 0);
| private void | registerForSettingsChanges()Observes watchdogs secure setting changes.
ContentObserver contentObserver = new ContentObserver(this.getHandler()) {
@Override
public void onChange(boolean selfChange) {
sendMessage(EVENT_WATCHDOG_SETTINGS_CHANGE);
}
};
mContext.getContentResolver().registerContentObserver(
Settings.Global.getUriFor(Settings.Global.WIFI_WATCHDOG_POOR_NETWORK_TEST_ENABLED),
false, contentObserver);
| private void | registerForWatchdogToggle()Observes the watchdog on/off setting, and takes action when changed.
ContentObserver contentObserver = new ContentObserver(this.getHandler()) {
@Override
public void onChange(boolean selfChange) {
sendMessage(EVENT_WATCHDOG_TOGGLED);
}
};
mContext.getContentResolver().registerContentObserver(
Settings.Global.getUriFor(Settings.Global.WIFI_WATCHDOG_ON),
false, contentObserver);
| private void | sendLinkStatusNotification(boolean isGood)
if (DBG) logd("########################################");
if (isGood) {
mWsmChannel.sendMessage(GOOD_LINK_DETECTED);
if (mCurrentBssid != null) {
mCurrentBssid.mLastTimeGood = SystemClock.elapsedRealtime();
}
if (DBG) logd("Good link notification is sent");
} else {
mWsmChannel.sendMessage(POOR_LINK_DETECTED);
if (mCurrentBssid != null) {
mCurrentBssid.mLastTimePoor = SystemClock.elapsedRealtime();
}
logd("Poor link notification is sent");
}
| private void | setupNetworkReceiver()
mBroadcastReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
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));
}
}
};
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.SUPPLICANT_STATE_CHANGED_ACTION);
mIntentFilter.addAction(Intent.ACTION_SCREEN_ON);
mIntentFilter.addAction(Intent.ACTION_SCREEN_OFF);
mContext.registerReceiver(mBroadcastReceiver, mIntentFilter);
| private void | updateCurrentBssid(java.lang.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 void | updateSettings()
if (DBG) logd("Updating secure settings");
// Unconditionally disable poor network avoidance, since this mechanism is obsolete
mPoorNetworkDetectionEnabled = false;
|
|