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