/*
 * Copyright (C) 2011 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package android.net.wifi;

import android.content.BroadcastReceiver;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.database.ContentObserver;
import android.net.ConnectivityManager;
import android.net.LinkProperties;
import android.net.NetworkInfo;
import android.net.wifi.RssiPacketCountInfo;
import android.os.Message;
import android.os.SystemClock;
import android.provider.Settings;
import android.provider.Settings.Secure;
import android.util.Log;
import android.util.LruCache;

import com.android.internal.R;
import com.android.internal.util.AsyncChannel;
import com.android.internal.util.Protocol;
import com.android.internal.util.State;
import com.android.internal.util.StateMachine;

import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.text.DecimalFormat;

/**
 * WifiWatchdogStateMachine monitors the connection to a WiFi network. When WiFi
 * connects at L2 layer, the beacons from access point reach the device and it
 * can maintain a connection, but the application connectivity can be flaky (due
 * to bigger packet size exchange).
 * <p>
 * We now monitor the quality of the last hop on WiFi using packet loss ratio as
 * an indicator to decide if the link is good enough to switch to Wi-Fi as the
 * uplink.
 * <p>
 * When WiFi is connected, the WiFi watchdog keeps sampling the RSSI and the
 * instant packet loss, and record it as per-AP loss-to-rssi statistics. When
 * the instant packet loss is higher than a threshold, the WiFi watchdog sends a
 * poor link notification to avoid WiFi connection temporarily.
 * <p>
 * While WiFi is being avoided, the WiFi watchdog keep watching the RSSI to
 * bring the WiFi connection back. Once the RSSI is high enough to achieve a
 * lower packet loss, a good link detection is sent such that the WiFi
 * connection become available again.
 * <p>
 * BSSID roaming has been taken into account. When user is moving across
 * multiple APs, the WiFi watchdog will detect that and keep watching the
 * currently connected AP.
 * <p>
 * Power impact should be minimal since much of the measurement relies on
 * passive statistics already being tracked at the driver and the polling is
 * done when screen is turned on and the RSSI is in a certain range.
 *
 * @hide
 */
public class WifiWatchdogStateMachine extends StateMachine {

    private static final boolean DBG = false;

    private static final int BASE = Protocol.BASE_WIFI_WATCHDOG;

    /* Internal events */
    private static final int EVENT_WATCHDOG_TOGGLED                 = BASE + 1;
    private static final int EVENT_NETWORK_STATE_CHANGE             = BASE + 2;
    private static final int EVENT_RSSI_CHANGE                      = BASE + 3;
    private static final int EVENT_SUPPLICANT_STATE_CHANGE          = BASE + 4;
    private static final int EVENT_WIFI_RADIO_STATE_CHANGE          = BASE + 5;
    private static final int EVENT_WATCHDOG_SETTINGS_CHANGE         = BASE + 6;
    private static final int EVENT_BSSID_CHANGE                     = BASE + 7;
    private static final int EVENT_SCREEN_ON                        = BASE + 8;
    private static final int EVENT_SCREEN_OFF                       = BASE + 9;

    /* Internal messages */
    private static final int CMD_RSSI_FETCH                         = BASE + 11;

    /* Notifications from/to WifiStateMachine */
    static final int POOR_LINK_DETECTED                             = BASE + 21;
    static final int GOOD_LINK_DETECTED                             = BASE + 22;

    public static final boolean DEFAULT_POOR_NETWORK_AVOIDANCE_ENABLED = false;

    /*
     * RSSI levels as used by notification icon
     * Level 4  -55 <= RSSI
     * Level 3  -66 <= RSSI < -55
     * Level 2  -77 <= RSSI < -67
     * Level 1  -88 <= RSSI < -78
     * Level 0         RSSI < -88
     */

    /**
     * WiFi link statistics is monitored and recorded actively below this threshold.
     * <p>
     * Larger threshold is more adaptive but increases sampling cost.
     */
    private static final int LINK_MONITOR_LEVEL_THRESHOLD = WifiManager.RSSI_LEVELS - 1;

    /**
     * Remember packet loss statistics of how many BSSIDs.
     * <p>
     * Larger size is usually better but requires more space.
     */
    private static final int BSSID_STAT_CACHE_SIZE = 20;

    /**
     * RSSI range of a BSSID statistics.
     * Within the range, (RSSI -> packet loss %) mappings are stored.
     * <p>
     * Larger range is usually better but requires more space.
     */
    private static final int BSSID_STAT_RANGE_LOW_DBM  = -105;

    /**
     * See {@link #BSSID_STAT_RANGE_LOW_DBM}.
     */
    private static final int BSSID_STAT_RANGE_HIGH_DBM = -45;

    /**
     * How many consecutive empty data point to trigger a empty-cache detection.
     * In this case, a preset/default loss value (function on RSSI) is used.
     * <p>
     * In normal uses, some RSSI values may never be seen due to channel randomness.
     * However, the size of such empty RSSI chunk in normal use is generally 1~2.
     */
    private static final int BSSID_STAT_EMPTY_COUNT = 3;

    /**
     * Sample interval for packet loss statistics, in msec.
     * <p>
     * Smaller interval is more accurate but increases sampling cost (battery consumption).
     */
    private static final long LINK_SAMPLING_INTERVAL_MS = 1 * 1000;

    /**
     * Coefficients (alpha) for moving average for packet loss tracking.
     * Must be within (0.0, 1.0).
     * <p>
     * Equivalent number of samples: N = 2 / alpha - 1 .
     * We want the historic loss to base on more data points to be statistically reliable.
     * We want the current instant loss to base on less data points to be responsive.
     */
    private static final double EXP_COEFFICIENT_RECORD  = 0.1;

    /**
     * See {@link #EXP_COEFFICIENT_RECORD}.
     */
    private static final double EXP_COEFFICIENT_MONITOR = 0.5;

    /**
     * Thresholds for sending good/poor link notifications, in packet loss %.
     * Good threshold must be smaller than poor threshold.
     * Use smaller poor threshold to avoid WiFi more aggressively.
     * Use smaller good threshold to bring back WiFi more conservatively.
     * <p>
     * When approaching the boundary, loss ratio jumps significantly within a few dBs.
     * 50% loss threshold is a good balance between accuracy and reponsiveness.
     * <=10% good threshold is a safe value to avoid jumping back to WiFi too easily.
     */
    private static final double POOR_LINK_LOSS_THRESHOLD = 0.5;

    /**
     * See {@link #POOR_LINK_LOSS_THRESHOLD}.
     */
    private static final double GOOD_LINK_LOSS_THRESHOLD = 0.1;

    /**
     * Number of samples to confirm before sending a poor link notification.
     * Response time = confirm_count * sample_interval .
     * <p>
     * A smaller threshold improves response speed but may suffer from randomness.
     * According to experiments, 3~5 are good values to achieve a balance.
     * These parameters should be tuned along with {@link #LINK_SAMPLING_INTERVAL_MS}.
     */
    private static final int POOR_LINK_SAMPLE_COUNT = 3;

    /**
     * Minimum volume (converted from pkt/sec) to detect a poor link, to avoid randomness.
     * <p>
     * According to experiments, 1pkt/sec is too sensitive but 3pkt/sec is slightly unresponsive.
     */
    private static final double POOR_LINK_MIN_VOLUME = 2.0 * LINK_SAMPLING_INTERVAL_MS / 1000.0;

    /**
     * When a poor link is detected, we scan over this range (based on current
     * poor link RSSI) for a target RSSI that satisfies a target packet loss.
     * Refer to {@link #GOOD_LINK_TARGET}.
     * <p>
     * We want range_min not too small to avoid jumping back to WiFi too easily.
     */
    private static final int GOOD_LINK_RSSI_RANGE_MIN = 3;

    /**
     * See {@link #GOOD_LINK_RSSI_RANGE_MIN}.
     */
    private static final int GOOD_LINK_RSSI_RANGE_MAX = 20;

    /**
     * Adaptive good link target to avoid flapping.
     * When a poor link is detected, a good link target is calculated as follows:
     * <p>
     *      targetRSSI = min { rssi | loss(rssi) < GOOD_LINK_LOSS_THRESHOLD } + rssi_adj[i],
     *                   where rssi is within the above GOOD_LINK_RSSI_RANGE.
     *      targetCount = sample_count[i] .
     * <p>
     * While WiFi is being avoided, we keep monitoring its signal strength.
     * Good link notification is sent when we see current RSSI >= targetRSSI
     * for targetCount consecutive times.
     * <p>
     * Index i is incremented each time after a poor link detection.
     * Index i is decreased to at most k if the last poor link was at lease reduce_time[k] ago.
     * <p>
     * Intuitively, larger index i makes it more difficult to get back to WiFi, avoiding flapping.
     * In experiments, (+9 dB / 30 counts) makes it quite difficult to achieve.
     * Avoid using it unless flapping is really bad (say, last poor link is < 1 min ago).
     */
    private static final GoodLinkTarget[] GOOD_LINK_TARGET = {
        /*                  rssi_adj,       sample_count,   reduce_time */
        new GoodLinkTarget( 0,              3,              30 * 60000   ),
        new GoodLinkTarget( 3,              5,              5  * 60000   ),
        new GoodLinkTarget( 6,              10,             1  * 60000   ),
        new GoodLinkTarget( 9,              30,             0  * 60000   ),
    };

    /**
     * The max time to avoid a BSSID, to prevent avoiding forever.
     * If current RSSI is at least min_rssi[i], the max avoidance time is at most max_time[i]
     * <p>
     * It is unusual to experience high packet loss at high RSSI. Something unusual must be
     * happening (e.g. strong interference). For higher signal strengths, we set the avoidance
     * time to be low to allow for quick turn around from temporary interference.
     * <p>
     * See {@link BssidStatistics#poorLinkDetected}.
     */
    private static final MaxAvoidTime[] MAX_AVOID_TIME = {
        /*                  max_time,           min_rssi */
        new MaxAvoidTime(   30 * 60000,         -200      ),
        new MaxAvoidTime(   5  * 60000,         -70       ),
        new MaxAvoidTime(   0  * 60000,         -55       ),
    };

    /* Framework related */
    private Context mContext;
    private ContentResolver mContentResolver;
    private WifiManager mWifiManager;
    private IntentFilter mIntentFilter;
    private BroadcastReceiver mBroadcastReceiver;
    private AsyncChannel mWsmChannel = new AsyncChannel();
    private WifiInfo mWifiInfo;
    private LinkProperties mLinkProperties;

    /* System settingss related */
    private static boolean sWifiOnly = false;
    private boolean mPoorNetworkDetectionEnabled;

    /* Poor link detection related */
    private LruCache<String, BssidStatistics> mBssidCache =
            new LruCache<String, BssidStatistics>(BSSID_STAT_CACHE_SIZE);
    private int mRssiFetchToken = 0;
    private int mCurrentSignalLevel;
    private BssidStatistics mCurrentBssid;
    private VolumeWeightedEMA mCurrentLoss;
    private boolean mIsScreenOn = true;
    private static double sPresetLoss[];

    /* WiFi watchdog state machine related */
    private DefaultState mDefaultState = new DefaultState();
    private WatchdogDisabledState mWatchdogDisabledState = new WatchdogDisabledState();
    private WatchdogEnabledState mWatchdogEnabledState = new WatchdogEnabledState();
    private NotConnectedState mNotConnectedState = new NotConnectedState();
    private VerifyingLinkState mVerifyingLinkState = new VerifyingLinkState();
    private ConnectedState mConnectedState = new ConnectedState();
    private OnlineWatchState mOnlineWatchState = new OnlineWatchState();
    private LinkMonitoringState mLinkMonitoringState = new LinkMonitoringState();
    private OnlineState mOnlineState = new OnlineState();

    /**
     * STATE MAP
     *          Default
     *         /       \
     * Disabled      Enabled
     *             /     \     \
     * NotConnected  Verifying  Connected
     *                         /---------\
     *                       (all other states)
     */
    private WifiWatchdogStateMachine(Context context) {
        super("WifiWatchdogStateMachine");
        mContext = context;
        mContentResolver = context.getContentResolver();
        mWifiManager = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
        mWsmChannel.connectSync(mContext, getHandler(),
                mWifiManager.getWifiStateMachineMessenger());

        setupNetworkReceiver();

        // the content observer to listen needs a handler
        registerForSettingsChanges();
        registerForWatchdogToggle();
        addState(mDefaultState);
            addState(mWatchdogDisabledState, mDefaultState);
            addState(mWatchdogEnabledState, mDefaultState);
                addState(mNotConnectedState, mWatchdogEnabledState);
                addState(mVerifyingLinkState, mWatchdogEnabledState);
                addState(mConnectedState, mWatchdogEnabledState);
                    addState(mOnlineWatchState, mConnectedState);
                    addState(mLinkMonitoringState, mConnectedState);
                    addState(mOnlineState, mConnectedState);

        if (isWatchdogEnabled()) {
            setInitialState(mNotConnectedState);
        } else {
            setInitialState(mWatchdogDisabledState);
        }
        setLogRecSize(25);
        setLogOnlyTransitions(true);
        updateSettings();
    }

    public static WifiWatchdogStateMachine makeWifiWatchdogStateMachine(Context context) {
        ContentResolver contentResolver = context.getContentResolver();

        ConnectivityManager cm = (ConnectivityManager) context.getSystemService(
                Context.CONNECTIVITY_SERVICE);
        sWifiOnly = (cm.isNetworkSupported(ConnectivityManager.TYPE_MOBILE) == false);

        // Watchdog is always enabled. Poor network detection can be seperately turned on/off
        // TODO: Remove this setting & clean up state machine since we always have
        // watchdog in an enabled state
        putSettingsGlobalBoolean(contentResolver, Settings.Global.WIFI_WATCHDOG_ON, true);

        WifiWatchdogStateMachine wwsm = new WifiWatchdogStateMachine(context);
        wwsm.start();
        return wwsm;
    }

    private void setupNetworkReceiver() {
        mBroadcastReceiver = new BroadcastReceiver() {
            @Override
            public void onReceive(Context context, Intent intent) {
                String action = intent.getAction();
                if (action.equals(WifiManager.RSSI_CHANGED_ACTION)) {
                    obtainMessage(EVENT_RSSI_CHANGE,
                            intent.getIntExtra(WifiManager.EXTRA_NEW_RSSI, -200), 0).sendToTarget();
                } else if (action.equals(WifiManager.SUPPLICANT_STATE_CHANGED_ACTION)) {
                    sendMessage(EVENT_SUPPLICANT_STATE_CHANGE, intent);
                } else if (action.equals(WifiManager.NETWORK_STATE_CHANGED_ACTION)) {
                    sendMessage(EVENT_NETWORK_STATE_CHANGE, intent);
                } else if (action.equals(Intent.ACTION_SCREEN_ON)) {
                    sendMessage(EVENT_SCREEN_ON);
                } else if (action.equals(Intent.ACTION_SCREEN_OFF)) {
                    sendMessage(EVENT_SCREEN_OFF);
                } else if (action.equals(WifiManager.WIFI_STATE_CHANGED_ACTION)) {
                    sendMessage(EVENT_WIFI_RADIO_STATE_CHANGE,intent.getIntExtra(
                            WifiManager.EXTRA_WIFI_STATE, WifiManager.WIFI_STATE_UNKNOWN));
                }
            }
        };

        mIntentFilter = new IntentFilter();
        mIntentFilter.addAction(WifiManager.NETWORK_STATE_CHANGED_ACTION);
        mIntentFilter.addAction(WifiManager.WIFI_STATE_CHANGED_ACTION);
        mIntentFilter.addAction(WifiManager.RSSI_CHANGED_ACTION);
        mIntentFilter.addAction(WifiManager.SUPPLICANT_STATE_CHANGED_ACTION);
        mIntentFilter.addAction(Intent.ACTION_SCREEN_ON);
        mIntentFilter.addAction(Intent.ACTION_SCREEN_OFF);
        mContext.registerReceiver(mBroadcastReceiver, mIntentFilter);
    }

    /**
     * Observes the watchdog on/off setting, and takes action when changed.
     */
    private void registerForWatchdogToggle() {
        ContentObserver contentObserver = new ContentObserver(this.getHandler()) {
            @Override
            public void onChange(boolean selfChange) {
                sendMessage(EVENT_WATCHDOG_TOGGLED);
            }
        };

        mContext.getContentResolver().registerContentObserver(
                Settings.Global.getUriFor(Settings.Global.WIFI_WATCHDOG_ON),
                false, contentObserver);
    }

    /**
     * Observes watchdogs secure setting changes.
     */
    private void registerForSettingsChanges() {
        ContentObserver contentObserver = new ContentObserver(this.getHandler()) {
            @Override
            public void onChange(boolean selfChange) {
                sendMessage(EVENT_WATCHDOG_SETTINGS_CHANGE);
            }
        };

        mContext.getContentResolver().registerContentObserver(
                Settings.Global.getUriFor(Settings.Global.WIFI_WATCHDOG_POOR_NETWORK_TEST_ENABLED),
                false, contentObserver);
    }

    public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
        super.dump(fd, pw, args);
        pw.println("mWifiInfo: [" + mWifiInfo + "]");
        pw.println("mLinkProperties: [" + mLinkProperties + "]");
        pw.println("mCurrentSignalLevel: [" + mCurrentSignalLevel + "]");
        pw.println("mPoorNetworkDetectionEnabled: [" + mPoorNetworkDetectionEnabled + "]");
    }

    private boolean isWatchdogEnabled() {
        boolean ret = getSettingsGlobalBoolean(
                mContentResolver, Settings.Global.WIFI_WATCHDOG_ON, true);
        if (DBG) logd("Watchdog enabled " + ret);
        return ret;
    }

    private void updateSettings() {
        if (DBG) logd("Updating secure settings");

        // disable poor network avoidance
        if (sWifiOnly) {
            logd("Disabling poor network avoidance for wi-fi only device");
            mPoorNetworkDetectionEnabled = false;
        } else {
            mPoorNetworkDetectionEnabled = getSettingsGlobalBoolean(mContentResolver,
                    Settings.Global.WIFI_WATCHDOG_POOR_NETWORK_TEST_ENABLED,
                    DEFAULT_POOR_NETWORK_AVOIDANCE_ENABLED);
        }
    }

    /**
     * Default state, guard for unhandled messages.
     */
    class DefaultState extends State {
        @Override
        public void enter() {
            if (DBG) logd(getName());
        }

        @Override
        public boolean processMessage(Message msg) {
            switch (msg.what) {
                case EVENT_WATCHDOG_SETTINGS_CHANGE:
                    updateSettings();
                    if (DBG) logd("Updating wifi-watchdog secure settings");
                    break;
                case EVENT_RSSI_CHANGE:
                    mCurrentSignalLevel = calculateSignalLevel(msg.arg1);
                    break;
                case EVENT_WIFI_RADIO_STATE_CHANGE:
                case EVENT_NETWORK_STATE_CHANGE:
                case EVENT_SUPPLICANT_STATE_CHANGE:
                case EVENT_BSSID_CHANGE:
                case CMD_RSSI_FETCH:
                case WifiManager.RSSI_PKTCNT_FETCH_SUCCEEDED:
                case WifiManager.RSSI_PKTCNT_FETCH_FAILED:
                    // ignore
                    break;
                case EVENT_SCREEN_ON:
                    mIsScreenOn = true;
                    break;
                case EVENT_SCREEN_OFF:
                    mIsScreenOn = false;
                    break;
                default:
                    loge("Unhandled message " + msg + " in state " + getCurrentState().getName());
                    break;
            }
            return HANDLED;
        }
    }

    /**
     * WiFi watchdog is disabled by the setting.
     */
    class WatchdogDisabledState extends State {
        @Override
        public void enter() {
            if (DBG) logd(getName());
        }

        @Override
        public boolean processMessage(Message msg) {
            switch (msg.what) {
                case EVENT_WATCHDOG_TOGGLED:
                    if (isWatchdogEnabled())
                        transitionTo(mNotConnectedState);
                    return HANDLED;
                case EVENT_NETWORK_STATE_CHANGE:
                    Intent intent = (Intent) msg.obj;
                    NetworkInfo networkInfo = (NetworkInfo)
                            intent.getParcelableExtra(WifiManager.EXTRA_NETWORK_INFO);

                    switch (networkInfo.getDetailedState()) {
                        case VERIFYING_POOR_LINK:
                            if (DBG) logd("Watchdog disabled, verify link");
                            sendLinkStatusNotification(true);
                            break;
                        default:
                            break;
                    }
                    break;
            }
            return NOT_HANDLED;
        }
    }

    /**
     * WiFi watchdog is enabled by the setting.
     */
    class WatchdogEnabledState extends State {
        @Override
        public void enter() {
            if (DBG) logd(getName());
        }

        @Override
        public boolean processMessage(Message msg) {
            Intent intent;
            switch (msg.what) {
                case EVENT_WATCHDOG_TOGGLED:
                    if (!isWatchdogEnabled())
                        transitionTo(mWatchdogDisabledState);
                    break;

                case EVENT_NETWORK_STATE_CHANGE:
                    intent = (Intent) msg.obj;
                    NetworkInfo networkInfo =
                            (NetworkInfo) intent.getParcelableExtra(WifiManager.EXTRA_NETWORK_INFO);
                    if (DBG) logd("Network state change " + networkInfo.getDetailedState());

                    mWifiInfo = (WifiInfo) intent.getParcelableExtra(WifiManager.EXTRA_WIFI_INFO);
                    updateCurrentBssid(mWifiInfo != null ? mWifiInfo.getBSSID() : null);

                    switch (networkInfo.getDetailedState()) {
                        case VERIFYING_POOR_LINK:
                            mLinkProperties = (LinkProperties) intent.getParcelableExtra(
                                    WifiManager.EXTRA_LINK_PROPERTIES);
                            if (mPoorNetworkDetectionEnabled) {
                                if (mWifiInfo == null || mCurrentBssid == null) {
                                    loge("Ignore, wifiinfo " + mWifiInfo +" bssid " + mCurrentBssid);
                                    sendLinkStatusNotification(true);
                                } else {
                                    transitionTo(mVerifyingLinkState);
                                }
                            } else {
                                sendLinkStatusNotification(true);
                            }
                            break;
                        case CONNECTED:
                            transitionTo(mOnlineWatchState);
                            break;
                        default:
                            transitionTo(mNotConnectedState);
                            break;
                    }
                    break;

                case EVENT_SUPPLICANT_STATE_CHANGE:
                    intent = (Intent) msg.obj;
                    SupplicantState supplicantState = (SupplicantState) intent.getParcelableExtra(
                            WifiManager.EXTRA_NEW_STATE);
                    if (supplicantState == SupplicantState.COMPLETED) {
                        mWifiInfo = mWifiManager.getConnectionInfo();
                        updateCurrentBssid(mWifiInfo.getBSSID());
                    }
                    break;

                case EVENT_WIFI_RADIO_STATE_CHANGE:
                    if (msg.arg1 == WifiManager.WIFI_STATE_DISABLING) {
                        transitionTo(mNotConnectedState);
                    }
                    break;

                default:
                    return NOT_HANDLED;
            }

            return HANDLED;
        }
    }

    /**
     * WiFi is disconnected.
     */
    class NotConnectedState extends State {
        @Override
        public void enter() {
            if (DBG) logd(getName());
        }
    }

    /**
     * WiFi is connected, but waiting for good link detection message.
     */
    class VerifyingLinkState extends State {

        private int mSampleCount;

        @Override
        public void enter() {
            if (DBG) logd(getName());
            mSampleCount = 0;
            mCurrentBssid.newLinkDetected();
            sendMessage(obtainMessage(CMD_RSSI_FETCH, ++mRssiFetchToken, 0));
        }

        @Override
        public boolean processMessage(Message msg) {
            switch (msg.what) {
                case EVENT_WATCHDOG_SETTINGS_CHANGE:
                    updateSettings();
                    if (!mPoorNetworkDetectionEnabled) {
                        sendLinkStatusNotification(true);
                    }
                    break;

                case EVENT_BSSID_CHANGE:
                    transitionTo(mVerifyingLinkState);
                    break;

                case CMD_RSSI_FETCH:
                    if (msg.arg1 == mRssiFetchToken) {
                        mWsmChannel.sendMessage(WifiManager.RSSI_PKTCNT_FETCH);
                        sendMessageDelayed(obtainMessage(CMD_RSSI_FETCH, ++mRssiFetchToken, 0),
                                LINK_SAMPLING_INTERVAL_MS);
                    }
                    break;

                case WifiManager.RSSI_PKTCNT_FETCH_SUCCEEDED:
                    RssiPacketCountInfo info = (RssiPacketCountInfo) msg.obj;
                    int rssi = info.rssi;
                    if (DBG) logd("Fetch RSSI succeed, rssi=" + rssi);

                    long time = mCurrentBssid.mBssidAvoidTimeMax - SystemClock.elapsedRealtime();
                    if (time <= 0) {
                        // max avoidance time is met
                        if (DBG) logd("Max avoid time elapsed");
                        sendLinkStatusNotification(true);
                    } else {
                        if (rssi >= mCurrentBssid.mGoodLinkTargetRssi) {
                            if (++mSampleCount >= mCurrentBssid.mGoodLinkTargetCount) {
                                // link is good again
                                if (DBG) logd("Good link detected, rssi=" + rssi);
                                mCurrentBssid.mBssidAvoidTimeMax = 0;
                                sendLinkStatusNotification(true);
                            }
                        } else {
                            mSampleCount = 0;
                            if (DBG) logd("Link is still poor, time left=" + time);
                        }
                    }
                    break;

                case WifiManager.RSSI_PKTCNT_FETCH_FAILED:
                    if (DBG) logd("RSSI_FETCH_FAILED");
                    break;

                default:
                    return NOT_HANDLED;
            }
            return HANDLED;
        }
    }

    /**
     * WiFi is connected and link is verified.
     */
    class ConnectedState extends State {
        @Override
        public void enter() {
            if (DBG) logd(getName());
        }

        @Override
        public boolean processMessage(Message msg) {
            switch (msg.what) {
                case EVENT_WATCHDOG_SETTINGS_CHANGE:
                    updateSettings();
                    if (mPoorNetworkDetectionEnabled) {
                        transitionTo(mOnlineWatchState);
                    } else {
                        transitionTo(mOnlineState);
                    }
                    return HANDLED;
            }
            return NOT_HANDLED;
        }
    }

    /**
     * RSSI is high enough and don't need link monitoring.
     */
    class OnlineWatchState extends State {
        @Override
        public void enter() {
            if (DBG) logd(getName());
            if (mPoorNetworkDetectionEnabled) {
                // treat entry as an rssi change
                handleRssiChange();
            } else {
                transitionTo(mOnlineState);
            }
        }

        private void handleRssiChange() {
            if (mCurrentSignalLevel <= LINK_MONITOR_LEVEL_THRESHOLD && mCurrentBssid != null) {
                transitionTo(mLinkMonitoringState);
            } else {
                // stay here
            }
        }

        @Override
        public boolean processMessage(Message msg) {
            switch (msg.what) {
                case EVENT_RSSI_CHANGE:
                    mCurrentSignalLevel = calculateSignalLevel(msg.arg1);
                    handleRssiChange();
                    break;
                default:
                    return NOT_HANDLED;
            }
            return HANDLED;
        }
    }

    /**
     * Keep sampling the link and monitor any poor link situation.
     */
    class LinkMonitoringState extends State {

        private int mSampleCount;

        private int mLastRssi;
        private int mLastTxGood;
        private int mLastTxBad;

        @Override
        public void enter() {
            if (DBG) logd(getName());
            mSampleCount = 0;
            mCurrentLoss = new VolumeWeightedEMA(EXP_COEFFICIENT_MONITOR);
            sendMessage(obtainMessage(CMD_RSSI_FETCH, ++mRssiFetchToken, 0));
        }

        @Override
        public boolean processMessage(Message msg) {
            switch (msg.what) {
                case EVENT_RSSI_CHANGE:
                    mCurrentSignalLevel = calculateSignalLevel(msg.arg1);
                    if (mCurrentSignalLevel <= LINK_MONITOR_LEVEL_THRESHOLD) {
                        // stay here;
                    } else {
                        // we don't need frequent RSSI monitoring any more
                        transitionTo(mOnlineWatchState);
                    }
                    break;

                case EVENT_BSSID_CHANGE:
                    transitionTo(mLinkMonitoringState);
                    break;

                case CMD_RSSI_FETCH:
                    if (!mIsScreenOn) {
                        transitionTo(mOnlineState);
                    } else if (msg.arg1 == mRssiFetchToken) {
                        mWsmChannel.sendMessage(WifiManager.RSSI_PKTCNT_FETCH);
                        sendMessageDelayed(obtainMessage(CMD_RSSI_FETCH, ++mRssiFetchToken, 0),
                                LINK_SAMPLING_INTERVAL_MS);
                    }
                    break;

                case WifiManager.RSSI_PKTCNT_FETCH_SUCCEEDED:
                    RssiPacketCountInfo info = (RssiPacketCountInfo) msg.obj;
                    int rssi = info.rssi;
                    int mrssi = (mLastRssi + rssi) / 2;
                    int txbad = info.txbad;
                    int txgood = info.txgood;
                    if (DBG) logd("Fetch RSSI succeed, rssi=" + rssi + " mrssi=" + mrssi + " txbad="
                            + txbad + " txgood=" + txgood);

                    // skip the first data point as we want incremental values
                    long now = SystemClock.elapsedRealtime();
                    if (now - mCurrentBssid.mLastTimeSample < LINK_SAMPLING_INTERVAL_MS * 2) {

                        // update packet loss statistics
                        int dbad = txbad - mLastTxBad;
                        int dgood = txgood - mLastTxGood;
                        int dtotal = dbad + dgood;

                        if (dtotal > 0) {
                            // calculate packet loss in the last sampling interval
                            double loss = ((double) dbad) / ((double) dtotal);

                            mCurrentLoss.update(loss, dtotal);

                            if (DBG) {
                                DecimalFormat df = new DecimalFormat("#.##");
                                logd("Incremental loss=" + dbad + "/" + dtotal + " Current loss="
                                        + df.format(mCurrentLoss.mValue * 100) + "% volume="
                                        + df.format(mCurrentLoss.mVolume));
                            }

                            mCurrentBssid.updateLoss(mrssi, loss, dtotal);

                            // check for high packet loss and send poor link notification
                            if (mCurrentLoss.mValue > POOR_LINK_LOSS_THRESHOLD
                                    && mCurrentLoss.mVolume > POOR_LINK_MIN_VOLUME) {
                                if (++mSampleCount >= POOR_LINK_SAMPLE_COUNT)
                                    if (mCurrentBssid.poorLinkDetected(rssi)) {
                                        sendLinkStatusNotification(false);
                                        ++mRssiFetchToken;
                                    }
                            } else {
                                mSampleCount = 0;
                            }
                        }
                    }

                    mCurrentBssid.mLastTimeSample = now;
                    mLastTxBad = txbad;
                    mLastTxGood = txgood;
                    mLastRssi = rssi;
                    break;

                case WifiManager.RSSI_PKTCNT_FETCH_FAILED:
                    // can happen if we are waiting to get a disconnect notification
                    if (DBG) logd("RSSI_FETCH_FAILED");
                    break;

                default:
                    return NOT_HANDLED;
            }
            return HANDLED;
        }
   }

    /**
     * Child state of ConnectedState indicating that we are online and there is nothing to do.
     */
    class OnlineState extends State {
        @Override
        public void enter() {
            if (DBG) logd(getName());
        }

        @Override
        public boolean processMessage(Message msg) {
            switch (msg.what) {
                case EVENT_SCREEN_ON:
                    mIsScreenOn = true;
                    if (mPoorNetworkDetectionEnabled)
                        transitionTo(mOnlineWatchState);
                    break;
                default:
                    return NOT_HANDLED;
            }
            return HANDLED;
        }
    }

    private void updateCurrentBssid(String bssid) {
        if (DBG) logd("Update current BSSID to " + (bssid != null ? bssid : "null"));

        // if currently not connected, then set current BSSID to null
        if (bssid == null) {
            if (mCurrentBssid == null) return;
            mCurrentBssid = null;
            if (DBG) logd("BSSID changed");
            sendMessage(EVENT_BSSID_CHANGE);
            return;
        }

        // if it is already the current BSSID, then done
        if (mCurrentBssid != null && bssid.equals(mCurrentBssid.mBssid)) return;

        // search for the new BSSID in the cache, add to cache if not found
        mCurrentBssid = mBssidCache.get(bssid);
        if (mCurrentBssid == null) {
            mCurrentBssid = new BssidStatistics(bssid);
            mBssidCache.put(bssid, mCurrentBssid);
        }

        // send BSSID change notification
        if (DBG) logd("BSSID changed");
        sendMessage(EVENT_BSSID_CHANGE);
    }

    private int calculateSignalLevel(int rssi) {
        int signalLevel = WifiManager.calculateSignalLevel(rssi, WifiManager.RSSI_LEVELS);
        if (DBG)
            logd("RSSI current: " + mCurrentSignalLevel + " new: " + rssi + ", " + signalLevel);
        return signalLevel;
    }

    private void sendLinkStatusNotification(boolean isGood) {
        if (DBG) logd("########################################");
        if (isGood) {
            mWsmChannel.sendMessage(GOOD_LINK_DETECTED);
            if (mCurrentBssid != null) {
                mCurrentBssid.mLastTimeGood = SystemClock.elapsedRealtime();
            }
            if (DBG) logd("Good link notification is sent");
        } else {
            mWsmChannel.sendMessage(POOR_LINK_DETECTED);
            if (mCurrentBssid != null) {
                mCurrentBssid.mLastTimePoor = SystemClock.elapsedRealtime();
            }
            logd("Poor link notification is sent");
        }
    }

    /**
     * Convenience function for retrieving a single secure settings value as a
     * boolean. Note that internally setting values are always stored as
     * strings; this function converts the string to a boolean for you. The
     * default value will be returned if the setting is not defined or not a
     * valid boolean.
     *
     * @param cr The ContentResolver to access.
     * @param name The name of the setting to retrieve.
     * @param def Value to return if the setting is not defined.
     * @return The setting's current value, or 'def' if it is not defined or not
     *         a valid boolean.
     */
    private static boolean getSettingsGlobalBoolean(ContentResolver cr, String name, boolean def) {
        return Settings.Global.getInt(cr, name, def ? 1 : 0) == 1;
    }

    /**
     * Convenience function for updating a single settings value as an integer.
     * This will either create a new entry in the table if the given name does
     * not exist, or modify the value of the existing row with that name. Note
     * that internally setting values are always stored as strings, so this
     * function converts the given value to a string before storing it.
     *
     * @param cr The ContentResolver to access.
     * @param name The name of the setting to modify.
     * @param value The new value for the setting.
     * @return true if the value was set, false on database errors
     */
    private static boolean putSettingsGlobalBoolean(ContentResolver cr, String name, boolean value) {
        return Settings.Global.putInt(cr, name, value ? 1 : 0);
    }

    /**
     * Bundle of good link count parameters
     */
    private static class GoodLinkTarget {
        public final int RSSI_ADJ_DBM;
        public final int SAMPLE_COUNT;
        public final int REDUCE_TIME_MS;
        public GoodLinkTarget(int adj, int count, int time) {
            RSSI_ADJ_DBM = adj;
            SAMPLE_COUNT = count;
            REDUCE_TIME_MS = time;
        }
    }

    /**
     * Bundle of max avoidance time parameters
     */
    private static class MaxAvoidTime {
        public final int TIME_MS;
        public final int MIN_RSSI_DBM;
        public MaxAvoidTime(int time, int rssi) {
            TIME_MS = time;
            MIN_RSSI_DBM = rssi;
        }
    }

    /**
     * Volume-weighted Exponential Moving Average (V-EMA)
     *    - volume-weighted:  each update has its own weight (number of packets)
     *    - exponential:      O(1) time and O(1) space for both update and query
     *    - moving average:   reflect most recent results and expire old ones
     */
    private class VolumeWeightedEMA {
        private double mValue;
        private double mVolume;
        private double mProduct;
        private final double mAlpha;

        public VolumeWeightedEMA(double coefficient) {
            mValue   = 0.0;
            mVolume  = 0.0;
            mProduct = 0.0;
            mAlpha   = coefficient;
        }

        public void update(double newValue, int newVolume) {
            if (newVolume <= 0) return;
            // core update formulas
            double newProduct = newValue * newVolume;
            mProduct = mAlpha * newProduct + (1 - mAlpha) * mProduct;
            mVolume  = mAlpha * newVolume  + (1 - mAlpha) * mVolume;
            mValue   = mProduct / mVolume;
        }
    }

    /**
     * Record (RSSI -> pakce loss %) mappings of one BSSID
     */
    private class BssidStatistics {

        /* MAC address of this BSSID */
        private final String mBssid;

        /* RSSI -> packet loss % mappings */
        private VolumeWeightedEMA[] mEntries;
        private int mRssiBase;
        private int mEntriesSize;

        /* Target to send good link notification, set when poor link is detected */
        private int mGoodLinkTargetRssi;
        private int mGoodLinkTargetCount;

        /* Index of GOOD_LINK_TARGET array */
        private int mGoodLinkTargetIndex;

        /* Timestamps of some last events */
        private long mLastTimeSample;
        private long mLastTimeGood;
        private long mLastTimePoor;

        /* Max time to avoid this BSSID */
        private long mBssidAvoidTimeMax;

        /**
         * Constructor
         *
         * @param bssid is the address of this BSSID
         */
        public BssidStatistics(String bssid) {
            this.mBssid = bssid;
            mRssiBase = BSSID_STAT_RANGE_LOW_DBM;
            mEntriesSize = BSSID_STAT_RANGE_HIGH_DBM - BSSID_STAT_RANGE_LOW_DBM + 1;
            mEntries = new VolumeWeightedEMA[mEntriesSize];
            for (int i = 0; i < mEntriesSize; i++)
                mEntries[i] = new VolumeWeightedEMA(EXP_COEFFICIENT_RECORD);
        }

        /**
         * Update this BSSID cache
         *
         * @param rssi is the RSSI
         * @param value is the new instant loss value at this RSSI
         * @param volume is the volume for this single update
         */
        public void updateLoss(int rssi, double value, int volume) {
            if (volume <= 0) return;
            int index = rssi - mRssiBase;
            if (index < 0 || index >= mEntriesSize) return;
            mEntries[index].update(value, volume);
            if (DBG) {
                DecimalFormat df = new DecimalFormat("#.##");
                logd("Cache updated: loss[" + rssi + "]=" + df.format(mEntries[index].mValue * 100)
                        + "% volume=" + df.format(mEntries[index].mVolume));
            }
        }

        /**
         * Get preset loss if the cache has insufficient data, observed from experiments.
         *
         * @param rssi is the input RSSI
         * @return preset loss of the given RSSI
         */
        public double presetLoss(int rssi) {
            if (rssi <= -90) return 1.0;
            if (rssi > 0) return 0.0;

            if (sPresetLoss == null) {
                // pre-calculate all preset losses only once, then reuse them
                final int size = 90;
                sPresetLoss = new double[size];
                for (int i = 0; i < size; i++) sPresetLoss[i] = 1.0 / Math.pow(90 - i, 1.5);
            }
            return sPresetLoss[-rssi];
        }

        /**
         * A poor link is detected, calculate a target RSSI to bring WiFi back.
         *
         * @param rssi is the current RSSI
         * @return true iff the current BSSID should be avoided
         */
        public boolean poorLinkDetected(int rssi) {
            if (DBG) logd("Poor link detected, rssi=" + rssi);

            long now = SystemClock.elapsedRealtime();
            long lastGood = now - mLastTimeGood;
            long lastPoor = now - mLastTimePoor;

            // reduce the difficulty of good link target if last avoidance was long time ago
            while (mGoodLinkTargetIndex > 0
                    && lastPoor >= GOOD_LINK_TARGET[mGoodLinkTargetIndex - 1].REDUCE_TIME_MS)
                mGoodLinkTargetIndex--;
            mGoodLinkTargetCount = GOOD_LINK_TARGET[mGoodLinkTargetIndex].SAMPLE_COUNT;

            // scan for a target RSSI at which the link is good
            int from = rssi + GOOD_LINK_RSSI_RANGE_MIN;
            int to = rssi + GOOD_LINK_RSSI_RANGE_MAX;
            mGoodLinkTargetRssi = findRssiTarget(from, to, GOOD_LINK_LOSS_THRESHOLD);
            mGoodLinkTargetRssi += GOOD_LINK_TARGET[mGoodLinkTargetIndex].RSSI_ADJ_DBM;
            if (mGoodLinkTargetIndex < GOOD_LINK_TARGET.length - 1) mGoodLinkTargetIndex++;

            // calculate max avoidance time to prevent avoiding forever
            int p = 0, pmax = MAX_AVOID_TIME.length - 1;
            while (p < pmax && rssi >= MAX_AVOID_TIME[p + 1].MIN_RSSI_DBM) p++;
            long avoidMax = MAX_AVOID_TIME[p].TIME_MS;

            // don't avoid if max avoidance time is 0 (RSSI is super high)
            if (avoidMax <= 0) return false;

            // set max avoidance time, send poor link notification
            mBssidAvoidTimeMax = now + avoidMax;

            if (DBG) logd("goodRssi=" + mGoodLinkTargetRssi + " goodCount=" + mGoodLinkTargetCount
                    + " lastGood=" + lastGood + " lastPoor=" + lastPoor + " avoidMax=" + avoidMax);

            return true;
        }

        /**
         * A new BSSID is connected, recalculate target RSSI threshold
         */
        public void newLinkDetected() {
            // if this BSSID is currently being avoided, the reuse those values
            if (mBssidAvoidTimeMax > 0) {
                if (DBG) logd("Previous avoidance still in effect, rssi=" + mGoodLinkTargetRssi
                        + " count=" + mGoodLinkTargetCount);
                return;
            }

            // calculate a new RSSI threshold for new link verifying
            int from = BSSID_STAT_RANGE_LOW_DBM;
            int to = BSSID_STAT_RANGE_HIGH_DBM;
            mGoodLinkTargetRssi = findRssiTarget(from, to, GOOD_LINK_LOSS_THRESHOLD);
            mGoodLinkTargetCount = 1;
            mBssidAvoidTimeMax = SystemClock.elapsedRealtime() + MAX_AVOID_TIME[0].TIME_MS;
            if (DBG) logd("New link verifying target set, rssi=" + mGoodLinkTargetRssi + " count="
                    + mGoodLinkTargetCount);
        }

        /**
         * Return the first RSSI within the range where loss[rssi] < threshold
         *
         * @param from start scanning from this RSSI
         * @param to stop scanning at this RSSI
         * @param threshold target threshold for scanning
         * @return target RSSI
         */
        public int findRssiTarget(int from, int to, double threshold) {
            from -= mRssiBase;
            to -= mRssiBase;
            int emptyCount = 0;
            int d = from < to ? 1 : -1;
            for (int i = from; i != to; i += d)
                // don't use a data point if it volume is too small (statistically unreliable)
                if (i >= 0 && i < mEntriesSize && mEntries[i].mVolume > 1.0) {
                    emptyCount = 0;
                    if (mEntries[i].mValue < threshold) {
                        // scan target found
                        int rssi = mRssiBase + i;
                        if (DBG) {
                            DecimalFormat df = new DecimalFormat("#.##");
                            logd("Scan target found: rssi=" + rssi + " threshold="
                                    + df.format(threshold * 100) + "% value="
                                    + df.format(mEntries[i].mValue * 100) + "% volume="
                                    + df.format(mEntries[i].mVolume));
                        }
                        return rssi;
                    }
                } else if (++emptyCount >= BSSID_STAT_EMPTY_COUNT) {
                    // cache has insufficient data around this RSSI, use preset loss instead
                    int rssi = mRssiBase + i;
                    double lossPreset = presetLoss(rssi);
                    if (lossPreset < threshold) {
                        if (DBG) {
                            DecimalFormat df = new DecimalFormat("#.##");
                            logd("Scan target found: rssi=" + rssi + " threshold="
                                    + df.format(threshold * 100) + "% value="
                                    + df.format(lossPreset * 100) + "% volume=preset");
                        }
                        return rssi;
                    }
                }

            return mRssiBase + to;
        }
    }
}
