1/*
2 * Copyright (C) 2011 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package android.net.wifi;
18
19import android.content.BroadcastReceiver;
20import android.content.ContentResolver;
21import android.content.Context;
22import android.content.Intent;
23import android.content.IntentFilter;
24import android.database.ContentObserver;
25import android.net.ConnectivityManager;
26import android.net.LinkProperties;
27import android.net.NetworkInfo;
28import android.net.wifi.RssiPacketCountInfo;
29import android.os.Message;
30import android.os.SystemClock;
31import android.provider.Settings;
32import android.provider.Settings.Secure;
33import android.util.Log;
34import android.util.LruCache;
35
36import com.android.internal.R;
37import com.android.internal.util.AsyncChannel;
38import com.android.internal.util.Protocol;
39import com.android.internal.util.State;
40import com.android.internal.util.StateMachine;
41
42import java.io.PrintWriter;
43import java.text.DecimalFormat;
44
45/**
46 * WifiWatchdogStateMachine monitors the connection to a WiFi network. When WiFi
47 * connects at L2 layer, the beacons from access point reach the device and it
48 * can maintain a connection, but the application connectivity can be flaky (due
49 * to bigger packet size exchange).
50 * <p>
51 * We now monitor the quality of the last hop on WiFi using packet loss ratio as
52 * an indicator to decide if the link is good enough to switch to Wi-Fi as the
53 * uplink.
54 * <p>
55 * When WiFi is connected, the WiFi watchdog keeps sampling the RSSI and the
56 * instant packet loss, and record it as per-AP loss-to-rssi statistics. When
57 * the instant packet loss is higher than a threshold, the WiFi watchdog sends a
58 * poor link notification to avoid WiFi connection temporarily.
59 * <p>
60 * While WiFi is being avoided, the WiFi watchdog keep watching the RSSI to
61 * bring the WiFi connection back. Once the RSSI is high enough to achieve a
62 * lower packet loss, a good link detection is sent such that the WiFi
63 * connection become available again.
64 * <p>
65 * BSSID roaming has been taken into account. When user is moving across
66 * multiple APs, the WiFi watchdog will detect that and keep watching the
67 * currently connected AP.
68 * <p>
69 * Power impact should be minimal since much of the measurement relies on
70 * passive statistics already being tracked at the driver and the polling is
71 * done when screen is turned on and the RSSI is in a certain range.
72 *
73 * @hide
74 */
75public class WifiWatchdogStateMachine extends StateMachine {
76
77    /* STOPSHIP: Keep this configurable for debugging until ship */
78    private static boolean DBG = false;
79    private static final String TAG = "WifiWatchdogStateMachine";
80
81    private static final int BASE = Protocol.BASE_WIFI_WATCHDOG;
82
83    /* Internal events */
84    private static final int EVENT_WATCHDOG_TOGGLED                 = BASE + 1;
85    private static final int EVENT_NETWORK_STATE_CHANGE             = BASE + 2;
86    private static final int EVENT_RSSI_CHANGE                      = BASE + 3;
87    private static final int EVENT_SUPPLICANT_STATE_CHANGE          = BASE + 4;
88    private static final int EVENT_WIFI_RADIO_STATE_CHANGE          = BASE + 5;
89    private static final int EVENT_WATCHDOG_SETTINGS_CHANGE         = BASE + 6;
90    private static final int EVENT_BSSID_CHANGE                     = BASE + 7;
91    private static final int EVENT_SCREEN_ON                        = BASE + 8;
92    private static final int EVENT_SCREEN_OFF                       = BASE + 9;
93
94    /* Internal messages */
95    private static final int CMD_RSSI_FETCH                         = BASE + 11;
96
97    /* Notifications from/to WifiStateMachine */
98    static final int POOR_LINK_DETECTED                             = BASE + 21;
99    static final int GOOD_LINK_DETECTED                             = BASE + 22;
100
101    public static final boolean DEFAULT_POOR_NETWORK_AVOIDANCE_ENABLED = false;
102
103    /*
104     * RSSI levels as used by notification icon
105     * Level 4  -55 <= RSSI
106     * Level 3  -66 <= RSSI < -55
107     * Level 2  -77 <= RSSI < -67
108     * Level 1  -88 <= RSSI < -78
109     * Level 0         RSSI < -88
110     */
111
112    /**
113     * WiFi link statistics is monitored and recorded actively below this threshold.
114     * <p>
115     * Larger threshold is more adaptive but increases sampling cost.
116     */
117    private static final int LINK_MONITOR_LEVEL_THRESHOLD = WifiManager.RSSI_LEVELS - 1;
118
119    /**
120     * Remember packet loss statistics of how many BSSIDs.
121     * <p>
122     * Larger size is usually better but requires more space.
123     */
124    private static final int BSSID_STAT_CACHE_SIZE = 20;
125
126    /**
127     * RSSI range of a BSSID statistics.
128     * Within the range, (RSSI -> packet loss %) mappings are stored.
129     * <p>
130     * Larger range is usually better but requires more space.
131     */
132    private static final int BSSID_STAT_RANGE_LOW_DBM  = -105;
133
134    /**
135     * See {@link #BSSID_STAT_RANGE_LOW_DBM}.
136     */
137    private static final int BSSID_STAT_RANGE_HIGH_DBM = -45;
138
139    /**
140     * How many consecutive empty data point to trigger a empty-cache detection.
141     * In this case, a preset/default loss value (function on RSSI) is used.
142     * <p>
143     * In normal uses, some RSSI values may never be seen due to channel randomness.
144     * However, the size of such empty RSSI chunk in normal use is generally 1~2.
145     */
146    private static final int BSSID_STAT_EMPTY_COUNT = 3;
147
148    /**
149     * Sample interval for packet loss statistics, in msec.
150     * <p>
151     * Smaller interval is more accurate but increases sampling cost (battery consumption).
152     */
153    private static final long LINK_SAMPLING_INTERVAL_MS = 1 * 1000;
154
155    /**
156     * Coefficients (alpha) for moving average for packet loss tracking.
157     * Must be within (0.0, 1.0).
158     * <p>
159     * Equivalent number of samples: N = 2 / alpha - 1 .
160     * We want the historic loss to base on more data points to be statistically reliable.
161     * We want the current instant loss to base on less data points to be responsive.
162     */
163    private static final double EXP_COEFFICIENT_RECORD  = 0.1;
164
165    /**
166     * See {@link #EXP_COEFFICIENT_RECORD}.
167     */
168    private static final double EXP_COEFFICIENT_MONITOR = 0.5;
169
170    /**
171     * Thresholds for sending good/poor link notifications, in packet loss %.
172     * Good threshold must be smaller than poor threshold.
173     * Use smaller poor threshold to avoid WiFi more aggressively.
174     * Use smaller good threshold to bring back WiFi more conservatively.
175     * <p>
176     * When approaching the boundary, loss ratio jumps significantly within a few dBs.
177     * 50% loss threshold is a good balance between accuracy and reponsiveness.
178     * <=10% good threshold is a safe value to avoid jumping back to WiFi too easily.
179     */
180    private static final double POOR_LINK_LOSS_THRESHOLD = 0.5;
181
182    /**
183     * See {@link #POOR_LINK_LOSS_THRESHOLD}.
184     */
185    private static final double GOOD_LINK_LOSS_THRESHOLD = 0.1;
186
187    /**
188     * Number of samples to confirm before sending a poor link notification.
189     * Response time = confirm_count * sample_interval .
190     * <p>
191     * A smaller threshold improves response speed but may suffer from randomness.
192     * According to experiments, 3~5 are good values to achieve a balance.
193     * These parameters should be tuned along with {@link #LINK_SAMPLING_INTERVAL_MS}.
194     */
195    private static final int POOR_LINK_SAMPLE_COUNT = 3;
196
197    /**
198     * Minimum volume (converted from pkt/sec) to detect a poor link, to avoid randomness.
199     * <p>
200     * According to experiments, 1pkt/sec is too sensitive but 3pkt/sec is slightly unresponsive.
201     */
202    private static final double POOR_LINK_MIN_VOLUME = 2.0 * LINK_SAMPLING_INTERVAL_MS / 1000.0;
203
204    /**
205     * When a poor link is detected, we scan over this range (based on current
206     * poor link RSSI) for a target RSSI that satisfies a target packet loss.
207     * Refer to {@link #GOOD_LINK_TARGET}.
208     * <p>
209     * We want range_min not too small to avoid jumping back to WiFi too easily.
210     */
211    private static final int GOOD_LINK_RSSI_RANGE_MIN = 3;
212
213    /**
214     * See {@link #GOOD_LINK_RSSI_RANGE_MIN}.
215     */
216    private static final int GOOD_LINK_RSSI_RANGE_MAX = 20;
217
218    /**
219     * Adaptive good link target to avoid flapping.
220     * When a poor link is detected, a good link target is calculated as follows:
221     * <p>
222     *      targetRSSI = min { rssi | loss(rssi) < GOOD_LINK_LOSS_THRESHOLD } + rssi_adj[i],
223     *                   where rssi is within the above GOOD_LINK_RSSI_RANGE.
224     *      targetCount = sample_count[i] .
225     * <p>
226     * While WiFi is being avoided, we keep monitoring its signal strength.
227     * Good link notification is sent when we see current RSSI >= targetRSSI
228     * for targetCount consecutive times.
229     * <p>
230     * Index i is incremented each time after a poor link detection.
231     * Index i is decreased to at most k if the last poor link was at lease reduce_time[k] ago.
232     * <p>
233     * Intuitively, larger index i makes it more difficult to get back to WiFi, avoiding flapping.
234     * In experiments, (+9 dB / 30 counts) makes it quite difficult to achieve.
235     * Avoid using it unless flapping is really bad (say, last poor link is < 1 min ago).
236     */
237    private static final GoodLinkTarget[] GOOD_LINK_TARGET = {
238        /*                  rssi_adj,       sample_count,   reduce_time */
239        new GoodLinkTarget( 0,              3,              30 * 60000   ),
240        new GoodLinkTarget( 3,              5,              5  * 60000   ),
241        new GoodLinkTarget( 6,              10,             1  * 60000   ),
242        new GoodLinkTarget( 9,              30,             0  * 60000   ),
243    };
244
245    /**
246     * The max time to avoid a BSSID, to prevent avoiding forever.
247     * If current RSSI is at least min_rssi[i], the max avoidance time is at most max_time[i]
248     * <p>
249     * It is unusual to experience high packet loss at high RSSI. Something unusual must be
250     * happening (e.g. strong interference). For higher signal strengths, we set the avoidance
251     * time to be low to allow for quick turn around from temporary interference.
252     * <p>
253     * See {@link BssidStatistics#poorLinkDetected}.
254     */
255    private static final MaxAvoidTime[] MAX_AVOID_TIME = {
256        /*                  max_time,           min_rssi */
257        new MaxAvoidTime(   30 * 60000,         -200      ),
258        new MaxAvoidTime(   5  * 60000,         -70       ),
259        new MaxAvoidTime(   0  * 60000,         -55       ),
260    };
261
262    /* Framework related */
263    private Context mContext;
264    private ContentResolver mContentResolver;
265    private WifiManager mWifiManager;
266    private IntentFilter mIntentFilter;
267    private BroadcastReceiver mBroadcastReceiver;
268    private AsyncChannel mWsmChannel = new AsyncChannel();
269    private WifiInfo mWifiInfo;
270    private LinkProperties mLinkProperties;
271
272    /* System settingss related */
273    private static boolean sWifiOnly = false;
274    private boolean mPoorNetworkDetectionEnabled;
275
276    /* Poor link detection related */
277    private LruCache<String, BssidStatistics> mBssidCache =
278            new LruCache<String, BssidStatistics>(BSSID_STAT_CACHE_SIZE);
279    private int mRssiFetchToken = 0;
280    private int mCurrentSignalLevel;
281    private BssidStatistics mCurrentBssid;
282    private VolumeWeightedEMA mCurrentLoss;
283    private boolean mIsScreenOn = true;
284    private static double sPresetLoss[];
285
286    /* WiFi watchdog state machine related */
287    private DefaultState mDefaultState = new DefaultState();
288    private WatchdogDisabledState mWatchdogDisabledState = new WatchdogDisabledState();
289    private WatchdogEnabledState mWatchdogEnabledState = new WatchdogEnabledState();
290    private NotConnectedState mNotConnectedState = new NotConnectedState();
291    private VerifyingLinkState mVerifyingLinkState = new VerifyingLinkState();
292    private ConnectedState mConnectedState = new ConnectedState();
293    private OnlineWatchState mOnlineWatchState = new OnlineWatchState();
294    private LinkMonitoringState mLinkMonitoringState = new LinkMonitoringState();
295    private OnlineState mOnlineState = new OnlineState();
296
297    /**
298     * STATE MAP
299     *          Default
300     *         /       \
301     * Disabled      Enabled
302     *             /     \     \
303     * NotConnected  Verifying  Connected
304     *                         /---------\
305     *                       (all other states)
306     */
307    private WifiWatchdogStateMachine(Context context) {
308        super(TAG);
309        mContext = context;
310        mContentResolver = context.getContentResolver();
311        mWifiManager = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
312        mWsmChannel.connectSync(mContext, getHandler(),
313                mWifiManager.getWifiStateMachineMessenger());
314
315        setupNetworkReceiver();
316
317        // the content observer to listen needs a handler
318        registerForSettingsChanges();
319        registerForWatchdogToggle();
320        addState(mDefaultState);
321            addState(mWatchdogDisabledState, mDefaultState);
322            addState(mWatchdogEnabledState, mDefaultState);
323                addState(mNotConnectedState, mWatchdogEnabledState);
324                addState(mVerifyingLinkState, mWatchdogEnabledState);
325                addState(mConnectedState, mWatchdogEnabledState);
326                    addState(mOnlineWatchState, mConnectedState);
327                    addState(mLinkMonitoringState, mConnectedState);
328                    addState(mOnlineState, mConnectedState);
329
330        if (isWatchdogEnabled()) {
331            setInitialState(mNotConnectedState);
332        } else {
333            setInitialState(mWatchdogDisabledState);
334        }
335        updateSettings();
336    }
337
338    public static WifiWatchdogStateMachine makeWifiWatchdogStateMachine(Context context) {
339        ContentResolver contentResolver = context.getContentResolver();
340
341        ConnectivityManager cm = (ConnectivityManager) context.getSystemService(
342                Context.CONNECTIVITY_SERVICE);
343        sWifiOnly = (cm.isNetworkSupported(ConnectivityManager.TYPE_MOBILE) == false);
344
345        // Watchdog is always enabled. Poor network detection can be seperately turned on/off
346        // TODO: Remove this setting & clean up state machine since we always have
347        // watchdog in an enabled state
348        putSettingsGlobalBoolean(contentResolver, Settings.Global.WIFI_WATCHDOG_ON, true);
349
350        WifiWatchdogStateMachine wwsm = new WifiWatchdogStateMachine(context);
351        wwsm.start();
352        return wwsm;
353    }
354
355    private void setupNetworkReceiver() {
356        mBroadcastReceiver = new BroadcastReceiver() {
357            @Override
358            public void onReceive(Context context, Intent intent) {
359                String action = intent.getAction();
360                if (action.equals(WifiManager.RSSI_CHANGED_ACTION)) {
361                    obtainMessage(EVENT_RSSI_CHANGE,
362                            intent.getIntExtra(WifiManager.EXTRA_NEW_RSSI, -200), 0).sendToTarget();
363                } else if (action.equals(WifiManager.SUPPLICANT_STATE_CHANGED_ACTION)) {
364                    sendMessage(EVENT_SUPPLICANT_STATE_CHANGE, intent);
365                } else if (action.equals(WifiManager.NETWORK_STATE_CHANGED_ACTION)) {
366                    sendMessage(EVENT_NETWORK_STATE_CHANGE, intent);
367                } else if (action.equals(Intent.ACTION_SCREEN_ON)) {
368                    sendMessage(EVENT_SCREEN_ON);
369                } else if (action.equals(Intent.ACTION_SCREEN_OFF)) {
370                    sendMessage(EVENT_SCREEN_OFF);
371                } else if (action.equals(WifiManager.WIFI_STATE_CHANGED_ACTION)) {
372                    sendMessage(EVENT_WIFI_RADIO_STATE_CHANGE,intent.getIntExtra(
373                            WifiManager.EXTRA_WIFI_STATE, WifiManager.WIFI_STATE_UNKNOWN));
374                }
375            }
376        };
377
378        mIntentFilter = new IntentFilter();
379        mIntentFilter.addAction(WifiManager.NETWORK_STATE_CHANGED_ACTION);
380        mIntentFilter.addAction(WifiManager.WIFI_STATE_CHANGED_ACTION);
381        mIntentFilter.addAction(WifiManager.RSSI_CHANGED_ACTION);
382        mIntentFilter.addAction(WifiManager.SUPPLICANT_STATE_CHANGED_ACTION);
383        mIntentFilter.addAction(Intent.ACTION_SCREEN_ON);
384        mIntentFilter.addAction(Intent.ACTION_SCREEN_OFF);
385        mContext.registerReceiver(mBroadcastReceiver, mIntentFilter);
386    }
387
388    /**
389     * Observes the watchdog on/off setting, and takes action when changed.
390     */
391    private void registerForWatchdogToggle() {
392        ContentObserver contentObserver = new ContentObserver(this.getHandler()) {
393            @Override
394            public void onChange(boolean selfChange) {
395                sendMessage(EVENT_WATCHDOG_TOGGLED);
396            }
397        };
398
399        mContext.getContentResolver().registerContentObserver(
400                Settings.Global.getUriFor(Settings.Global.WIFI_WATCHDOG_ON),
401                false, contentObserver);
402    }
403
404    /**
405     * Observes watchdogs secure setting changes.
406     */
407    private void registerForSettingsChanges() {
408        ContentObserver contentObserver = new ContentObserver(this.getHandler()) {
409            @Override
410            public void onChange(boolean selfChange) {
411                sendMessage(EVENT_WATCHDOG_SETTINGS_CHANGE);
412            }
413        };
414
415        mContext.getContentResolver().registerContentObserver(
416                Settings.Global.getUriFor(Settings.Global.WIFI_WATCHDOG_POOR_NETWORK_TEST_ENABLED),
417                false, contentObserver);
418    }
419
420    public void dump(PrintWriter pw) {
421        pw.print("WatchdogStatus: ");
422        pw.print("State: " + getCurrentState());
423        pw.println("mWifiInfo: [" + mWifiInfo + "]");
424        pw.println("mLinkProperties: [" + mLinkProperties + "]");
425        pw.println("mCurrentSignalLevel: [" + mCurrentSignalLevel + "]");
426        pw.println("mPoorNetworkDetectionEnabled: [" + mPoorNetworkDetectionEnabled + "]");
427    }
428
429    private boolean isWatchdogEnabled() {
430        boolean ret = getSettingsGlobalBoolean(
431                mContentResolver, Settings.Global.WIFI_WATCHDOG_ON, true);
432        if (DBG) logd("Watchdog enabled " + ret);
433        return ret;
434    }
435
436    private void updateSettings() {
437        if (DBG) logd("Updating secure settings");
438
439        // disable poor network avoidance
440        if (sWifiOnly) {
441            logd("Disabling poor network avoidance for wi-fi only device");
442            mPoorNetworkDetectionEnabled = false;
443        } else {
444            mPoorNetworkDetectionEnabled = getSettingsGlobalBoolean(mContentResolver,
445                    Settings.Global.WIFI_WATCHDOG_POOR_NETWORK_TEST_ENABLED,
446                    DEFAULT_POOR_NETWORK_AVOIDANCE_ENABLED);
447        }
448    }
449
450    /**
451     * Default state, guard for unhandled messages.
452     */
453    class DefaultState extends State {
454        @Override
455        public void enter() {
456            if (DBG) logd(getName());
457        }
458
459        @Override
460        public boolean processMessage(Message msg) {
461            switch (msg.what) {
462                case EVENT_WATCHDOG_SETTINGS_CHANGE:
463                    updateSettings();
464                    if (DBG) logd("Updating wifi-watchdog secure settings");
465                    break;
466                case EVENT_RSSI_CHANGE:
467                    mCurrentSignalLevel = calculateSignalLevel(msg.arg1);
468                    break;
469                case EVENT_WIFI_RADIO_STATE_CHANGE:
470                case EVENT_NETWORK_STATE_CHANGE:
471                case EVENT_SUPPLICANT_STATE_CHANGE:
472                case EVENT_BSSID_CHANGE:
473                case CMD_RSSI_FETCH:
474                case WifiManager.RSSI_PKTCNT_FETCH_SUCCEEDED:
475                case WifiManager.RSSI_PKTCNT_FETCH_FAILED:
476                    // ignore
477                    break;
478                case EVENT_SCREEN_ON:
479                    mIsScreenOn = true;
480                    break;
481                case EVENT_SCREEN_OFF:
482                    mIsScreenOn = false;
483                    break;
484                default:
485                    loge("Unhandled message " + msg + " in state " + getCurrentState().getName());
486                    break;
487            }
488            return HANDLED;
489        }
490    }
491
492    /**
493     * WiFi watchdog is disabled by the setting.
494     */
495    class WatchdogDisabledState extends State {
496        @Override
497        public void enter() {
498            if (DBG) logd(getName());
499        }
500
501        @Override
502        public boolean processMessage(Message msg) {
503            switch (msg.what) {
504                case EVENT_WATCHDOG_TOGGLED:
505                    if (isWatchdogEnabled())
506                        transitionTo(mNotConnectedState);
507                    return HANDLED;
508                case EVENT_NETWORK_STATE_CHANGE:
509                    Intent intent = (Intent) msg.obj;
510                    NetworkInfo networkInfo = (NetworkInfo)
511                            intent.getParcelableExtra(WifiManager.EXTRA_NETWORK_INFO);
512
513                    switch (networkInfo.getDetailedState()) {
514                        case VERIFYING_POOR_LINK:
515                            if (DBG) logd("Watchdog disabled, verify link");
516                            sendLinkStatusNotification(true);
517                            break;
518                        default:
519                            break;
520                    }
521                    break;
522            }
523            return NOT_HANDLED;
524        }
525    }
526
527    /**
528     * WiFi watchdog is enabled by the setting.
529     */
530    class WatchdogEnabledState extends State {
531        @Override
532        public void enter() {
533            if (DBG) logd(getName());
534        }
535
536        @Override
537        public boolean processMessage(Message msg) {
538            Intent intent;
539            switch (msg.what) {
540                case EVENT_WATCHDOG_TOGGLED:
541                    if (!isWatchdogEnabled())
542                        transitionTo(mWatchdogDisabledState);
543                    break;
544
545                case EVENT_NETWORK_STATE_CHANGE:
546                    intent = (Intent) msg.obj;
547                    NetworkInfo networkInfo =
548                            (NetworkInfo) intent.getParcelableExtra(WifiManager.EXTRA_NETWORK_INFO);
549                    if (DBG) logd("Network state change " + networkInfo.getDetailedState());
550
551                    mWifiInfo = (WifiInfo) intent.getParcelableExtra(WifiManager.EXTRA_WIFI_INFO);
552                    updateCurrentBssid(mWifiInfo != null ? mWifiInfo.getBSSID() : null);
553
554                    switch (networkInfo.getDetailedState()) {
555                        case VERIFYING_POOR_LINK:
556                            mLinkProperties = (LinkProperties) intent.getParcelableExtra(
557                                    WifiManager.EXTRA_LINK_PROPERTIES);
558                            if (mPoorNetworkDetectionEnabled) {
559                                if (mWifiInfo == null || mCurrentBssid == null) {
560                                    loge("Ignore, wifiinfo " + mWifiInfo +" bssid " + mCurrentBssid);
561                                    sendLinkStatusNotification(true);
562                                } else {
563                                    transitionTo(mVerifyingLinkState);
564                                }
565                            } else {
566                                sendLinkStatusNotification(true);
567                            }
568                            break;
569                        case CONNECTED:
570                            transitionTo(mOnlineWatchState);
571                            break;
572                        default:
573                            transitionTo(mNotConnectedState);
574                            break;
575                    }
576                    break;
577
578                case EVENT_SUPPLICANT_STATE_CHANGE:
579                    intent = (Intent) msg.obj;
580                    SupplicantState supplicantState = (SupplicantState) intent.getParcelableExtra(
581                            WifiManager.EXTRA_NEW_STATE);
582                    if (supplicantState == SupplicantState.COMPLETED) {
583                        mWifiInfo = mWifiManager.getConnectionInfo();
584                        updateCurrentBssid(mWifiInfo.getBSSID());
585                    }
586                    break;
587
588                case EVENT_WIFI_RADIO_STATE_CHANGE:
589                    if ((Integer) msg.obj == WifiManager.WIFI_STATE_DISABLING)
590                        transitionTo(mNotConnectedState);
591                    break;
592
593                default:
594                    return NOT_HANDLED;
595            }
596
597            return HANDLED;
598        }
599    }
600
601    /**
602     * WiFi is disconnected.
603     */
604    class NotConnectedState extends State {
605        @Override
606        public void enter() {
607            if (DBG) logd(getName());
608        }
609    }
610
611    /**
612     * WiFi is connected, but waiting for good link detection message.
613     */
614    class VerifyingLinkState extends State {
615
616        private int mSampleCount;
617
618        @Override
619        public void enter() {
620            if (DBG) logd(getName());
621            mSampleCount = 0;
622            mCurrentBssid.newLinkDetected();
623            sendMessage(obtainMessage(CMD_RSSI_FETCH, ++mRssiFetchToken, 0));
624        }
625
626        @Override
627        public boolean processMessage(Message msg) {
628            switch (msg.what) {
629                case EVENT_WATCHDOG_SETTINGS_CHANGE:
630                    updateSettings();
631                    if (!mPoorNetworkDetectionEnabled) {
632                        sendLinkStatusNotification(true);
633                    }
634                    break;
635
636                case EVENT_BSSID_CHANGE:
637                    transitionTo(mVerifyingLinkState);
638                    break;
639
640                case CMD_RSSI_FETCH:
641                    if (msg.arg1 == mRssiFetchToken) {
642                        mWsmChannel.sendMessage(WifiManager.RSSI_PKTCNT_FETCH);
643                        sendMessageDelayed(obtainMessage(CMD_RSSI_FETCH, ++mRssiFetchToken, 0),
644                                LINK_SAMPLING_INTERVAL_MS);
645                    }
646                    break;
647
648                case WifiManager.RSSI_PKTCNT_FETCH_SUCCEEDED:
649                    RssiPacketCountInfo info = (RssiPacketCountInfo) msg.obj;
650                    int rssi = info.rssi;
651                    if (DBG) logd("Fetch RSSI succeed, rssi=" + rssi);
652
653                    long time = mCurrentBssid.mBssidAvoidTimeMax - SystemClock.elapsedRealtime();
654                    if (time <= 0) {
655                        // max avoidance time is met
656                        if (DBG) logd("Max avoid time elapsed");
657                        sendLinkStatusNotification(true);
658                    } else {
659                        if (rssi >= mCurrentBssid.mGoodLinkTargetRssi) {
660                            if (++mSampleCount >= mCurrentBssid.mGoodLinkTargetCount) {
661                                // link is good again
662                                if (DBG) logd("Good link detected, rssi=" + rssi);
663                                mCurrentBssid.mBssidAvoidTimeMax = 0;
664                                sendLinkStatusNotification(true);
665                            }
666                        } else {
667                            mSampleCount = 0;
668                            if (DBG) logd("Link is still poor, time left=" + time);
669                        }
670                    }
671                    break;
672
673                case WifiManager.RSSI_PKTCNT_FETCH_FAILED:
674                    if (DBG) logd("RSSI_FETCH_FAILED");
675                    break;
676
677                default:
678                    return NOT_HANDLED;
679            }
680            return HANDLED;
681        }
682    }
683
684    /**
685     * WiFi is connected and link is verified.
686     */
687    class ConnectedState extends State {
688        @Override
689        public void enter() {
690            if (DBG) logd(getName());
691        }
692
693        @Override
694        public boolean processMessage(Message msg) {
695            switch (msg.what) {
696                case EVENT_WATCHDOG_SETTINGS_CHANGE:
697                    updateSettings();
698                    // STOPSHIP: Remove this at ship
699                    logd("Updated secure settings and turned debug on");
700                    DBG = true;
701
702                    if (mPoorNetworkDetectionEnabled) {
703                        transitionTo(mOnlineWatchState);
704                    } else {
705                        transitionTo(mOnlineState);
706                    }
707                    return HANDLED;
708            }
709            return NOT_HANDLED;
710        }
711    }
712
713    /**
714     * RSSI is high enough and don't need link monitoring.
715     */
716    class OnlineWatchState extends State {
717        @Override
718        public void enter() {
719            if (DBG) logd(getName());
720            if (mPoorNetworkDetectionEnabled) {
721                // treat entry as an rssi change
722                handleRssiChange();
723            } else {
724                transitionTo(mOnlineState);
725            }
726        }
727
728        private void handleRssiChange() {
729            if (mCurrentSignalLevel <= LINK_MONITOR_LEVEL_THRESHOLD && mCurrentBssid != null) {
730                transitionTo(mLinkMonitoringState);
731            } else {
732                // stay here
733            }
734        }
735
736        @Override
737        public boolean processMessage(Message msg) {
738            switch (msg.what) {
739                case EVENT_RSSI_CHANGE:
740                    mCurrentSignalLevel = calculateSignalLevel(msg.arg1);
741                    handleRssiChange();
742                    break;
743                default:
744                    return NOT_HANDLED;
745            }
746            return HANDLED;
747        }
748    }
749
750    /**
751     * Keep sampling the link and monitor any poor link situation.
752     */
753    class LinkMonitoringState extends State {
754
755        private int mSampleCount;
756
757        private int mLastRssi;
758        private int mLastTxGood;
759        private int mLastTxBad;
760
761        @Override
762        public void enter() {
763            if (DBG) logd(getName());
764            mSampleCount = 0;
765            mCurrentLoss = new VolumeWeightedEMA(EXP_COEFFICIENT_MONITOR);
766            sendMessage(obtainMessage(CMD_RSSI_FETCH, ++mRssiFetchToken, 0));
767        }
768
769        @Override
770        public boolean processMessage(Message msg) {
771            switch (msg.what) {
772                case EVENT_RSSI_CHANGE:
773                    mCurrentSignalLevel = calculateSignalLevel(msg.arg1);
774                    if (mCurrentSignalLevel <= LINK_MONITOR_LEVEL_THRESHOLD) {
775                        // stay here;
776                    } else {
777                        // we don't need frequent RSSI monitoring any more
778                        transitionTo(mOnlineWatchState);
779                    }
780                    break;
781
782                case EVENT_BSSID_CHANGE:
783                    transitionTo(mLinkMonitoringState);
784                    break;
785
786                case CMD_RSSI_FETCH:
787                    if (!mIsScreenOn) {
788                        transitionTo(mOnlineState);
789                    } else if (msg.arg1 == mRssiFetchToken) {
790                        mWsmChannel.sendMessage(WifiManager.RSSI_PKTCNT_FETCH);
791                        sendMessageDelayed(obtainMessage(CMD_RSSI_FETCH, ++mRssiFetchToken, 0),
792                                LINK_SAMPLING_INTERVAL_MS);
793                    }
794                    break;
795
796                case WifiManager.RSSI_PKTCNT_FETCH_SUCCEEDED:
797                    RssiPacketCountInfo info = (RssiPacketCountInfo) msg.obj;
798                    int rssi = info.rssi;
799                    int mrssi = (mLastRssi + rssi) / 2;
800                    int txbad = info.txbad;
801                    int txgood = info.txgood;
802                    if (DBG) logd("Fetch RSSI succeed, rssi=" + rssi + " mrssi=" + mrssi + " txbad="
803                            + txbad + " txgood=" + txgood);
804
805                    // skip the first data point as we want incremental values
806                    long now = SystemClock.elapsedRealtime();
807                    if (now - mCurrentBssid.mLastTimeSample < LINK_SAMPLING_INTERVAL_MS * 2) {
808
809                        // update packet loss statistics
810                        int dbad = txbad - mLastTxBad;
811                        int dgood = txgood - mLastTxGood;
812                        int dtotal = dbad + dgood;
813
814                        if (dtotal > 0) {
815                            // calculate packet loss in the last sampling interval
816                            double loss = ((double) dbad) / ((double) dtotal);
817
818                            mCurrentLoss.update(loss, dtotal);
819
820                            if (DBG) {
821                                DecimalFormat df = new DecimalFormat("#.##");
822                                logd("Incremental loss=" + dbad + "/" + dtotal + " Current loss="
823                                        + df.format(mCurrentLoss.mValue * 100) + "% volume="
824                                        + df.format(mCurrentLoss.mVolume));
825                            }
826
827                            mCurrentBssid.updateLoss(mrssi, loss, dtotal);
828
829                            // check for high packet loss and send poor link notification
830                            if (mCurrentLoss.mValue > POOR_LINK_LOSS_THRESHOLD
831                                    && mCurrentLoss.mVolume > POOR_LINK_MIN_VOLUME) {
832                                if (++mSampleCount >= POOR_LINK_SAMPLE_COUNT)
833                                    if (mCurrentBssid.poorLinkDetected(rssi)) {
834                                        sendLinkStatusNotification(false);
835                                        ++mRssiFetchToken;
836                                    }
837                            } else {
838                                mSampleCount = 0;
839                            }
840                        }
841                    }
842
843                    mCurrentBssid.mLastTimeSample = now;
844                    mLastTxBad = txbad;
845                    mLastTxGood = txgood;
846                    mLastRssi = rssi;
847                    break;
848
849                case WifiManager.RSSI_PKTCNT_FETCH_FAILED:
850                    // can happen if we are waiting to get a disconnect notification
851                    if (DBG) logd("RSSI_FETCH_FAILED");
852                    break;
853
854                default:
855                    return NOT_HANDLED;
856            }
857            return HANDLED;
858        }
859   }
860
861    /**
862     * Child state of ConnectedState indicating that we are online and there is nothing to do.
863     */
864    class OnlineState extends State {
865        @Override
866        public void enter() {
867            if (DBG) logd(getName());
868        }
869
870        @Override
871        public boolean processMessage(Message msg) {
872            switch (msg.what) {
873                case EVENT_SCREEN_ON:
874                    mIsScreenOn = true;
875                    if (mPoorNetworkDetectionEnabled)
876                        transitionTo(mOnlineWatchState);
877                    break;
878                default:
879                    return NOT_HANDLED;
880            }
881            return HANDLED;
882        }
883    }
884
885    private void updateCurrentBssid(String bssid) {
886        if (DBG) logd("Update current BSSID to " + (bssid != null ? bssid : "null"));
887
888        // if currently not connected, then set current BSSID to null
889        if (bssid == null) {
890            if (mCurrentBssid == null) return;
891            mCurrentBssid = null;
892            if (DBG) logd("BSSID changed");
893            sendMessage(EVENT_BSSID_CHANGE);
894            return;
895        }
896
897        // if it is already the current BSSID, then done
898        if (mCurrentBssid != null && bssid.equals(mCurrentBssid.mBssid)) return;
899
900        // search for the new BSSID in the cache, add to cache if not found
901        mCurrentBssid = mBssidCache.get(bssid);
902        if (mCurrentBssid == null) {
903            mCurrentBssid = new BssidStatistics(bssid);
904            mBssidCache.put(bssid, mCurrentBssid);
905        }
906
907        // send BSSID change notification
908        if (DBG) logd("BSSID changed");
909        sendMessage(EVENT_BSSID_CHANGE);
910    }
911
912    private int calculateSignalLevel(int rssi) {
913        int signalLevel = WifiManager.calculateSignalLevel(rssi, WifiManager.RSSI_LEVELS);
914        if (DBG)
915            logd("RSSI current: " + mCurrentSignalLevel + " new: " + rssi + ", " + signalLevel);
916        return signalLevel;
917    }
918
919    private void sendLinkStatusNotification(boolean isGood) {
920        if (DBG) logd("########################################");
921        if (isGood) {
922            mWsmChannel.sendMessage(GOOD_LINK_DETECTED);
923            if (mCurrentBssid != null) {
924                mCurrentBssid.mLastTimeGood = SystemClock.elapsedRealtime();
925            }
926            if (DBG) logd("Good link notification is sent");
927        } else {
928            mWsmChannel.sendMessage(POOR_LINK_DETECTED);
929            if (mCurrentBssid != null) {
930                mCurrentBssid.mLastTimePoor = SystemClock.elapsedRealtime();
931            }
932            logd("Poor link notification is sent");
933        }
934    }
935
936    /**
937     * Convenience function for retrieving a single secure settings value as a
938     * boolean. Note that internally setting values are always stored as
939     * strings; this function converts the string to a boolean for you. The
940     * default value will be returned if the setting is not defined or not a
941     * valid boolean.
942     *
943     * @param cr The ContentResolver to access.
944     * @param name The name of the setting to retrieve.
945     * @param def Value to return if the setting is not defined.
946     * @return The setting's current value, or 'def' if it is not defined or not
947     *         a valid boolean.
948     */
949    private static boolean getSettingsGlobalBoolean(ContentResolver cr, String name, boolean def) {
950        return Settings.Global.getInt(cr, name, def ? 1 : 0) == 1;
951    }
952
953    /**
954     * Convenience function for updating a single settings value as an integer.
955     * This will either create a new entry in the table if the given name does
956     * not exist, or modify the value of the existing row with that name. Note
957     * that internally setting values are always stored as strings, so this
958     * function converts the given value to a string before storing it.
959     *
960     * @param cr The ContentResolver to access.
961     * @param name The name of the setting to modify.
962     * @param value The new value for the setting.
963     * @return true if the value was set, false on database errors
964     */
965    private static boolean putSettingsGlobalBoolean(ContentResolver cr, String name, boolean value) {
966        return Settings.Global.putInt(cr, name, value ? 1 : 0);
967    }
968
969    private static void logd(String s) {
970        Log.d(TAG, s);
971    }
972
973    private static void loge(String s) {
974        Log.e(TAG, s);
975    }
976
977    /**
978     * Bundle of good link count parameters
979     */
980    private static class GoodLinkTarget {
981        public final int RSSI_ADJ_DBM;
982        public final int SAMPLE_COUNT;
983        public final int REDUCE_TIME_MS;
984        public GoodLinkTarget(int adj, int count, int time) {
985            RSSI_ADJ_DBM = adj;
986            SAMPLE_COUNT = count;
987            REDUCE_TIME_MS = time;
988        }
989    }
990
991    /**
992     * Bundle of max avoidance time parameters
993     */
994    private static class MaxAvoidTime {
995        public final int TIME_MS;
996        public final int MIN_RSSI_DBM;
997        public MaxAvoidTime(int time, int rssi) {
998            TIME_MS = time;
999            MIN_RSSI_DBM = rssi;
1000        }
1001    }
1002
1003    /**
1004     * Volume-weighted Exponential Moving Average (V-EMA)
1005     *    - volume-weighted:  each update has its own weight (number of packets)
1006     *    - exponential:      O(1) time and O(1) space for both update and query
1007     *    - moving average:   reflect most recent results and expire old ones
1008     */
1009    private class VolumeWeightedEMA {
1010        private double mValue;
1011        private double mVolume;
1012        private double mProduct;
1013        private final double mAlpha;
1014
1015        public VolumeWeightedEMA(double coefficient) {
1016            mValue   = 0.0;
1017            mVolume  = 0.0;
1018            mProduct = 0.0;
1019            mAlpha   = coefficient;
1020        }
1021
1022        public void update(double newValue, int newVolume) {
1023            if (newVolume <= 0) return;
1024            // core update formulas
1025            double newProduct = newValue * newVolume;
1026            mProduct = mAlpha * newProduct + (1 - mAlpha) * mProduct;
1027            mVolume  = mAlpha * newVolume  + (1 - mAlpha) * mVolume;
1028            mValue   = mProduct / mVolume;
1029        }
1030    }
1031
1032    /**
1033     * Record (RSSI -> pakce loss %) mappings of one BSSID
1034     */
1035    private class BssidStatistics {
1036
1037        /* MAC address of this BSSID */
1038        private final String mBssid;
1039
1040        /* RSSI -> packet loss % mappings */
1041        private VolumeWeightedEMA[] mEntries;
1042        private int mRssiBase;
1043        private int mEntriesSize;
1044
1045        /* Target to send good link notification, set when poor link is detected */
1046        private int mGoodLinkTargetRssi;
1047        private int mGoodLinkTargetCount;
1048
1049        /* Index of GOOD_LINK_TARGET array */
1050        private int mGoodLinkTargetIndex;
1051
1052        /* Timestamps of some last events */
1053        private long mLastTimeSample;
1054        private long mLastTimeGood;
1055        private long mLastTimePoor;
1056
1057        /* Max time to avoid this BSSID */
1058        private long mBssidAvoidTimeMax;
1059
1060        /**
1061         * Constructor
1062         *
1063         * @param bssid is the address of this BSSID
1064         */
1065        public BssidStatistics(String bssid) {
1066            this.mBssid = bssid;
1067            mRssiBase = BSSID_STAT_RANGE_LOW_DBM;
1068            mEntriesSize = BSSID_STAT_RANGE_HIGH_DBM - BSSID_STAT_RANGE_LOW_DBM + 1;
1069            mEntries = new VolumeWeightedEMA[mEntriesSize];
1070            for (int i = 0; i < mEntriesSize; i++)
1071                mEntries[i] = new VolumeWeightedEMA(EXP_COEFFICIENT_RECORD);
1072        }
1073
1074        /**
1075         * Update this BSSID cache
1076         *
1077         * @param rssi is the RSSI
1078         * @param value is the new instant loss value at this RSSI
1079         * @param volume is the volume for this single update
1080         */
1081        public void updateLoss(int rssi, double value, int volume) {
1082            if (volume <= 0) return;
1083            int index = rssi - mRssiBase;
1084            if (index < 0 || index >= mEntriesSize) return;
1085            mEntries[index].update(value, volume);
1086            if (DBG) {
1087                DecimalFormat df = new DecimalFormat("#.##");
1088                logd("Cache updated: loss[" + rssi + "]=" + df.format(mEntries[index].mValue * 100)
1089                        + "% volume=" + df.format(mEntries[index].mVolume));
1090            }
1091        }
1092
1093        /**
1094         * Get preset loss if the cache has insufficient data, observed from experiments.
1095         *
1096         * @param rssi is the input RSSI
1097         * @return preset loss of the given RSSI
1098         */
1099        public double presetLoss(int rssi) {
1100            if (rssi <= -90) return 1.0;
1101            if (rssi > 0) return 0.0;
1102
1103            if (sPresetLoss == null) {
1104                // pre-calculate all preset losses only once, then reuse them
1105                final int size = 90;
1106                sPresetLoss = new double[size];
1107                for (int i = 0; i < size; i++) sPresetLoss[i] = 1.0 / Math.pow(90 - i, 1.5);
1108            }
1109            return sPresetLoss[-rssi];
1110        }
1111
1112        /**
1113         * A poor link is detected, calculate a target RSSI to bring WiFi back.
1114         *
1115         * @param rssi is the current RSSI
1116         * @return true iff the current BSSID should be avoided
1117         */
1118        public boolean poorLinkDetected(int rssi) {
1119            if (DBG) logd("Poor link detected, rssi=" + rssi);
1120
1121            long now = SystemClock.elapsedRealtime();
1122            long lastGood = now - mLastTimeGood;
1123            long lastPoor = now - mLastTimePoor;
1124
1125            // reduce the difficulty of good link target if last avoidance was long time ago
1126            while (mGoodLinkTargetIndex > 0
1127                    && lastPoor >= GOOD_LINK_TARGET[mGoodLinkTargetIndex - 1].REDUCE_TIME_MS)
1128                mGoodLinkTargetIndex--;
1129            mGoodLinkTargetCount = GOOD_LINK_TARGET[mGoodLinkTargetIndex].SAMPLE_COUNT;
1130
1131            // scan for a target RSSI at which the link is good
1132            int from = rssi + GOOD_LINK_RSSI_RANGE_MIN;
1133            int to = rssi + GOOD_LINK_RSSI_RANGE_MAX;
1134            mGoodLinkTargetRssi = findRssiTarget(from, to, GOOD_LINK_LOSS_THRESHOLD);
1135            mGoodLinkTargetRssi += GOOD_LINK_TARGET[mGoodLinkTargetIndex].RSSI_ADJ_DBM;
1136            if (mGoodLinkTargetIndex < GOOD_LINK_TARGET.length - 1) mGoodLinkTargetIndex++;
1137
1138            // calculate max avoidance time to prevent avoiding forever
1139            int p = 0, pmax = MAX_AVOID_TIME.length - 1;
1140            while (p < pmax && rssi >= MAX_AVOID_TIME[p + 1].MIN_RSSI_DBM) p++;
1141            long avoidMax = MAX_AVOID_TIME[p].TIME_MS;
1142
1143            // don't avoid if max avoidance time is 0 (RSSI is super high)
1144            if (avoidMax <= 0) return false;
1145
1146            // set max avoidance time, send poor link notification
1147            mBssidAvoidTimeMax = now + avoidMax;
1148
1149            if (DBG) logd("goodRssi=" + mGoodLinkTargetRssi + " goodCount=" + mGoodLinkTargetCount
1150                    + " lastGood=" + lastGood + " lastPoor=" + lastPoor + " avoidMax=" + avoidMax);
1151
1152            return true;
1153        }
1154
1155        /**
1156         * A new BSSID is connected, recalculate target RSSI threshold
1157         */
1158        public void newLinkDetected() {
1159            // if this BSSID is currently being avoided, the reuse those values
1160            if (mBssidAvoidTimeMax > 0) {
1161                if (DBG) logd("Previous avoidance still in effect, rssi=" + mGoodLinkTargetRssi
1162                        + " count=" + mGoodLinkTargetCount);
1163                return;
1164            }
1165
1166            // calculate a new RSSI threshold for new link verifying
1167            int from = BSSID_STAT_RANGE_LOW_DBM;
1168            int to = BSSID_STAT_RANGE_HIGH_DBM;
1169            mGoodLinkTargetRssi = findRssiTarget(from, to, GOOD_LINK_LOSS_THRESHOLD);
1170            mGoodLinkTargetCount = 1;
1171            mBssidAvoidTimeMax = SystemClock.elapsedRealtime() + MAX_AVOID_TIME[0].TIME_MS;
1172            if (DBG) logd("New link verifying target set, rssi=" + mGoodLinkTargetRssi + " count="
1173                    + mGoodLinkTargetCount);
1174        }
1175
1176        /**
1177         * Return the first RSSI within the range where loss[rssi] < threshold
1178         *
1179         * @param from start scanning from this RSSI
1180         * @param to stop scanning at this RSSI
1181         * @param threshold target threshold for scanning
1182         * @return target RSSI
1183         */
1184        public int findRssiTarget(int from, int to, double threshold) {
1185            from -= mRssiBase;
1186            to -= mRssiBase;
1187            int emptyCount = 0;
1188            int d = from < to ? 1 : -1;
1189            for (int i = from; i != to; i += d)
1190                // don't use a data point if it volume is too small (statistically unreliable)
1191                if (i >= 0 && i < mEntriesSize && mEntries[i].mVolume > 1.0) {
1192                    emptyCount = 0;
1193                    if (mEntries[i].mValue < threshold) {
1194                        // scan target found
1195                        int rssi = mRssiBase + i;
1196                        if (DBG) {
1197                            DecimalFormat df = new DecimalFormat("#.##");
1198                            logd("Scan target found: rssi=" + rssi + " threshold="
1199                                    + df.format(threshold * 100) + "% value="
1200                                    + df.format(mEntries[i].mValue * 100) + "% volume="
1201                                    + df.format(mEntries[i].mVolume));
1202                        }
1203                        return rssi;
1204                    }
1205                } else if (++emptyCount >= BSSID_STAT_EMPTY_COUNT) {
1206                    // cache has insufficient data around this RSSI, use preset loss instead
1207                    int rssi = mRssiBase + i;
1208                    double lossPreset = presetLoss(rssi);
1209                    if (lossPreset < threshold) {
1210                        if (DBG) {
1211                            DecimalFormat df = new DecimalFormat("#.##");
1212                            logd("Scan target found: rssi=" + rssi + " threshold="
1213                                    + df.format(threshold * 100) + "% value="
1214                                    + df.format(lossPreset * 100) + "% volume=preset");
1215                        }
1216                        return rssi;
1217                    }
1218                }
1219
1220            return mRssiBase + to;
1221        }
1222    }
1223}
1224