/*
 * 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.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.res.Resources;
import android.database.ContentObserver;
import android.net.ConnectivityManager;
import android.net.DnsPinger;
import android.net.NetworkInfo;
import android.net.Uri;
import android.os.Message;
import android.os.SystemClock;
import android.os.SystemProperties;
import android.provider.Settings;
import android.provider.Settings.Secure;
import android.util.Log;

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

import java.io.IOException;
import java.io.PrintWriter;
import java.net.HttpURLConnection;
import java.net.InetAddress;
import java.net.URL;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;

/**
 * {@link WifiWatchdogStateMachine} monitors the initial connection to a Wi-Fi
 * network with multiple access points. After the framework successfully
 * connects to an access point, the watchdog verifies connectivity by 'pinging'
 * the configured DNS server using {@link DnsPinger}.
 * <p>
 * On DNS check failure, the BSSID is blacklisted if it is reasonably likely
 * that another AP might have internet access; otherwise the SSID is disabled.
 * <p>
 * On DNS success, the WatchdogService initiates a walled garden check via an
 * http get. A browser window is activated if a walled garden is detected.
 *
 * @hide
 */
public class WifiWatchdogStateMachine extends StateMachine {

    private static final boolean DBG = false;
    private static final String TAG = "WifiWatchdogStateMachine";
    private static final String DISABLED_NETWORK_NOTIFICATION_ID = "WifiWatchdog.networkdisabled";
    private static final String WALLED_GARDEN_NOTIFICATION_ID = "WifiWatchdog.walledgarden";

    private static final int WIFI_SIGNAL_LEVELS = 4;
    /**
     * Low signal is defined as less than or equal to cut off
     */
    private static final int LOW_SIGNAL_CUTOFF = 0;

    private static final long DEFAULT_DNS_CHECK_SHORT_INTERVAL_MS = 2 * 60 * 1000;
    private static final long DEFAULT_DNS_CHECK_LONG_INTERVAL_MS = 60 * 60 * 1000;
    private static final long DEFAULT_WALLED_GARDEN_INTERVAL_MS = 30 * 60 * 1000;

    private static final int DEFAULT_MAX_SSID_BLACKLISTS = 7;
    private static final int DEFAULT_NUM_DNS_PINGS = 5; // Multiple pings to detect setup issues
    private static final int DEFAULT_MIN_DNS_RESPONSES = 1;

    private static final int DEFAULT_DNS_PING_TIMEOUT_MS = 2000;

    private static final long DEFAULT_BLACKLIST_FOLLOWUP_INTERVAL_MS = 15 * 1000;

    // See http://go/clientsdns for usage approval
    private static final String DEFAULT_WALLED_GARDEN_URL =
            "http://clients3.google.com/generate_204";
    private static final int WALLED_GARDEN_SOCKET_TIMEOUT_MS = 10000;

    /* Some carrier apps might have support captive portal handling. Add some delay to allow
        app authentication to be done before our test.
       TODO: This should go away once we provide an API to apps to disable walled garden test
       for certain SSIDs
     */
    private static final int WALLED_GARDEN_START_DELAY_MS = 3000;

    private static final int DNS_INTRATEST_PING_INTERVAL_MS = 200;
    /* With some router setups, it takes a few hunder milli-seconds before connection is active */
    private static final int DNS_START_DELAY_MS = 1000;

    private static final int BASE = Protocol.BASE_WIFI_WATCHDOG;

    /**
     * Indicates the enable setting of WWS may have changed
     */
    private static final int EVENT_WATCHDOG_TOGGLED                 = BASE + 1;

    /**
     * Indicates the wifi network state has changed. Passed w/ original intent
     * which has a non-null networkInfo object
     */
    private static final int EVENT_NETWORK_STATE_CHANGE             = BASE + 2;
    /**
     * Indicates the signal has changed. Passed with arg1
     * {@link #mNetEventCounter} and arg2 [raw signal strength]
     */
    private static final int EVENT_RSSI_CHANGE                      = BASE + 3;
    private static final int EVENT_SCAN_RESULTS_AVAILABLE           = 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 MESSAGE_HANDLE_WALLED_GARDEN           = BASE + 100;
    private static final int MESSAGE_HANDLE_BAD_AP                  = BASE + 101;
    /**
     * arg1 == mOnlineWatchState.checkCount
     */
    private static final int MESSAGE_SINGLE_DNS_CHECK               = BASE + 102;
    private static final int MESSAGE_NETWORK_FOLLOWUP               = BASE + 103;
    private static final int MESSAGE_DELAYED_WALLED_GARDEN_CHECK    = BASE + 104;

    private Context mContext;
    private ContentResolver mContentResolver;
    private WifiManager mWifiManager;
    private DnsPinger mDnsPinger;
    private IntentFilter mIntentFilter;
    private BroadcastReceiver mBroadcastReceiver;

    private DefaultState mDefaultState = new DefaultState();
    private WatchdogDisabledState mWatchdogDisabledState = new WatchdogDisabledState();
    private WatchdogEnabledState mWatchdogEnabledState = new WatchdogEnabledState();
    private NotConnectedState mNotConnectedState = new NotConnectedState();
    private ConnectedState mConnectedState = new ConnectedState();
    private DnsCheckingState mDnsCheckingState = new DnsCheckingState();
    private OnlineWatchState mOnlineWatchState = new OnlineWatchState();
    private OnlineState mOnlineState = new OnlineState();
    private DnsCheckFailureState mDnsCheckFailureState = new DnsCheckFailureState();
    private DelayWalledGardenState mDelayWalledGardenState = new DelayWalledGardenState();
    private WalledGardenState mWalledGardenState = new WalledGardenState();
    private BlacklistedApState mBlacklistedApState = new BlacklistedApState();

    private long mDnsCheckShortIntervalMs;
    private long mDnsCheckLongIntervalMs;
    private long mWalledGardenIntervalMs;
    private int mMaxSsidBlacklists;
    private int mNumDnsPings;
    private int mMinDnsResponses;
    private int mDnsPingTimeoutMs;
    private long mBlacklistFollowupIntervalMs;
    private boolean mPoorNetworkDetectionEnabled;
    private boolean mWalledGardenTestEnabled;
    private String mWalledGardenUrl;

    private boolean mShowDisabledNotification;
    /**
     * The {@link WifiInfo} object passed to WWSM on network broadcasts
     */
    private WifiInfo mConnectionInfo;
    private int mNetEventCounter = 0;

    /**
     * Currently maintained but not used, TODO
     */
    private HashSet<String> mBssids = new HashSet<String>();
    private int mNumCheckFailures = 0;

    private Long mLastWalledGardenCheckTime = null;

    /**
     * This is set by the blacklisted state and reset when connected to a new AP.
     * It triggers a disableNetwork call if a DNS check fails.
     */
    public boolean mDisableAPNextFailure = false;
    private static boolean sWifiOnly = false;
    private boolean mDisabledNotificationShown;
    private boolean mWalledGardenNotificationShown;
    public boolean mHasConnectedWifiManager = false;

    /**
     * STATE MAP
     *          Default
     *         /       \
     * Disabled     Enabled
     *             /       \
     * NotConnected      Connected
     *                  /---------\
     *               (all other states)
     */
    private WifiWatchdogStateMachine(Context context) {
        super(TAG);
        mContext = context;
        mContentResolver = context.getContentResolver();
        mWifiManager = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
        mDnsPinger = new DnsPinger(mContext, "WifiWatchdogStateMachine.DnsPinger",
                                this.getHandler().getLooper(), this.getHandler(),
                                ConnectivityManager.TYPE_WIFI);

        setupNetworkReceiver();

        // The content observer to listen needs a handler
        registerForSettingsChanges();
        registerForWatchdogToggle();
        addState(mDefaultState);
            addState(mWatchdogDisabledState, mDefaultState);
            addState(mWatchdogEnabledState, mDefaultState);
                addState(mNotConnectedState, mWatchdogEnabledState);
                addState(mConnectedState, mWatchdogEnabledState);
                    addState(mDnsCheckingState, mConnectedState);
                    addState(mDnsCheckFailureState, mConnectedState);
                    addState(mDelayWalledGardenState, mConnectedState);
                    addState(mWalledGardenState, mConnectedState);
                    addState(mBlacklistedApState, mConnectedState);
                    addState(mOnlineWatchState, mConnectedState);
                    addState(mOnlineState, mConnectedState);

        setInitialState(mWatchdogDisabledState);
        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);

        // Disable for wifi only devices.
        if (Settings.Secure.getString(contentResolver, Settings.Secure.WIFI_WATCHDOG_ON) == null &&
                sWifiOnly) {
            putSettingsBoolean(contentResolver, Settings.Secure.WIFI_WATCHDOG_ON, false);
        }
        WifiWatchdogStateMachine wwsm = new WifiWatchdogStateMachine(context);
        wwsm.start();
        wwsm.sendMessage(EVENT_WATCHDOG_TOGGLED);
        return wwsm;
    }

    /**
   *
   */
    private void setupNetworkReceiver() {
        mBroadcastReceiver = new BroadcastReceiver() {
            @Override
            public void onReceive(Context context, Intent intent) {
                String action = intent.getAction();
                if (action.equals(WifiManager.NETWORK_STATE_CHANGED_ACTION)) {
                    sendMessage(EVENT_NETWORK_STATE_CHANGE, intent);
                } else if (action.equals(WifiManager.RSSI_CHANGED_ACTION)) {
                    obtainMessage(EVENT_RSSI_CHANGE, mNetEventCounter,
                            intent.getIntExtra(WifiManager.EXTRA_NEW_RSSI, -200)).sendToTarget();
                } else if (action.equals(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION)) {
                    sendMessage(EVENT_SCAN_RESULTS_AVAILABLE);
                } 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.SCAN_RESULTS_AVAILABLE_ACTION);
    }

    /**
     * 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.Secure.getUriFor(Settings.Secure.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.Secure.getUriFor(
                        Settings.Secure.WIFI_WATCHDOG_DNS_CHECK_SHORT_INTERVAL_MS),
                        false, contentObserver);
        mContext.getContentResolver().registerContentObserver(
                Settings.Secure.getUriFor(Settings.Secure.WIFI_WATCHDOG_DNS_CHECK_LONG_INTERVAL_MS),
                false, contentObserver);
        mContext.getContentResolver().registerContentObserver(
                Settings.Secure.getUriFor(Settings.Secure.WIFI_WATCHDOG_WALLED_GARDEN_INTERVAL_MS),
                false, contentObserver);
        mContext.getContentResolver().registerContentObserver(
                Settings.Secure.getUriFor(Settings.Secure.WIFI_WATCHDOG_MAX_SSID_BLACKLISTS),
                false, contentObserver);
        mContext.getContentResolver().registerContentObserver(
                Settings.Secure.getUriFor(Settings.Secure.WIFI_WATCHDOG_NUM_DNS_PINGS),
                false, contentObserver);
        mContext.getContentResolver().registerContentObserver(
                Settings.Secure.getUriFor(Settings.Secure.WIFI_WATCHDOG_MIN_DNS_RESPONSES),
                false, contentObserver);
        mContext.getContentResolver().registerContentObserver(
                Settings.Secure.getUriFor(Settings.Secure.WIFI_WATCHDOG_DNS_PING_TIMEOUT_MS),
                false, contentObserver);
        mContext.getContentResolver().registerContentObserver(
                Settings.Secure.getUriFor(
                        Settings.Secure.WIFI_WATCHDOG_BLACKLIST_FOLLOWUP_INTERVAL_MS),
                        false, contentObserver);
        mContext.getContentResolver().registerContentObserver(
                Settings.Secure.getUriFor(Settings.Secure.WIFI_WATCHDOG_WALLED_GARDEN_TEST_ENABLED),
                false, contentObserver);
        mContext.getContentResolver().registerContentObserver(
                Settings.Secure.getUriFor(Settings.Secure.WIFI_WATCHDOG_WALLED_GARDEN_URL),
                false, contentObserver);
        mContext.getContentResolver().registerContentObserver(
                Settings.Secure.getUriFor(Settings.Secure.WIFI_WATCHDOG_SHOW_DISABLED_NETWORK_POPUP)
                , false, contentObserver);
    }

    /**
     * DNS based detection techniques do not work at all hotspots. The one sure
     * way to check a walled garden is to see if a URL fetch on a known address
     * fetches the data we expect
     */
    private boolean isWalledGardenConnection() {
        HttpURLConnection urlConnection = null;
        try {
            URL url = new URL(mWalledGardenUrl);
            urlConnection = (HttpURLConnection) url.openConnection();
            urlConnection.setInstanceFollowRedirects(false);
            urlConnection.setConnectTimeout(WALLED_GARDEN_SOCKET_TIMEOUT_MS);
            urlConnection.setReadTimeout(WALLED_GARDEN_SOCKET_TIMEOUT_MS);
            urlConnection.setUseCaches(false);
            urlConnection.getInputStream();
            // We got a valid response, but not from the real google
            return urlConnection.getResponseCode() != 204;
        } catch (IOException e) {
            if (DBG) {
                log("Walled garden check - probably not a portal: exception " + e);
            }
            return false;
        } finally {
            if (urlConnection != null) {
                urlConnection.disconnect();
            }
        }
    }

    private boolean rssiStrengthAboveCutoff(int rssi) {
        return WifiManager.calculateSignalLevel(rssi, WIFI_SIGNAL_LEVELS) > LOW_SIGNAL_CUTOFF;
    }

    public void dump(PrintWriter pw) {
        pw.print("WatchdogStatus: ");
        pw.print("State " + getCurrentState());
        pw.println(", network [" + mConnectionInfo + "]");
        pw.print("checkFailures   " + mNumCheckFailures);
        pw.println(", bssids: " + mBssids);
        pw.println("lastSingleCheck: " + mOnlineWatchState.lastCheckTime);
    }

    private boolean isWatchdogEnabled() {
        return getSettingsBoolean(mContentResolver, Settings.Secure.WIFI_WATCHDOG_ON, true);
    }

    private void updateSettings() {
        mDnsCheckShortIntervalMs = Secure.getLong(mContentResolver,
                Secure.WIFI_WATCHDOG_DNS_CHECK_SHORT_INTERVAL_MS,
                DEFAULT_DNS_CHECK_SHORT_INTERVAL_MS);
        mDnsCheckLongIntervalMs = Secure.getLong(mContentResolver,
                Secure.WIFI_WATCHDOG_DNS_CHECK_LONG_INTERVAL_MS,
                DEFAULT_DNS_CHECK_LONG_INTERVAL_MS);
        mMaxSsidBlacklists = Secure.getInt(mContentResolver,
                Secure.WIFI_WATCHDOG_MAX_SSID_BLACKLISTS,
                DEFAULT_MAX_SSID_BLACKLISTS);
        mNumDnsPings = Secure.getInt(mContentResolver,
                Secure.WIFI_WATCHDOG_NUM_DNS_PINGS,
                DEFAULT_NUM_DNS_PINGS);
        mMinDnsResponses = Secure.getInt(mContentResolver,
                Secure.WIFI_WATCHDOG_MIN_DNS_RESPONSES,
                DEFAULT_MIN_DNS_RESPONSES);
        mDnsPingTimeoutMs = Secure.getInt(mContentResolver,
                Secure.WIFI_WATCHDOG_DNS_PING_TIMEOUT_MS,
                DEFAULT_DNS_PING_TIMEOUT_MS);
        mBlacklistFollowupIntervalMs = Secure.getLong(mContentResolver,
                Settings.Secure.WIFI_WATCHDOG_BLACKLIST_FOLLOWUP_INTERVAL_MS,
                DEFAULT_BLACKLIST_FOLLOWUP_INTERVAL_MS);
        //TODO: enable this by default after changing watchdog behavior
        //Also, update settings description
        mPoorNetworkDetectionEnabled = getSettingsBoolean(mContentResolver,
                Settings.Secure.WIFI_WATCHDOG_POOR_NETWORK_TEST_ENABLED, false);
        mWalledGardenTestEnabled = getSettingsBoolean(mContentResolver,
                Settings.Secure.WIFI_WATCHDOG_WALLED_GARDEN_TEST_ENABLED, true);
        mWalledGardenUrl = getSettingsStr(mContentResolver,
                Settings.Secure.WIFI_WATCHDOG_WALLED_GARDEN_URL,
                DEFAULT_WALLED_GARDEN_URL);
        mWalledGardenIntervalMs = Secure.getLong(mContentResolver,
                Secure.WIFI_WATCHDOG_WALLED_GARDEN_INTERVAL_MS,
                DEFAULT_WALLED_GARDEN_INTERVAL_MS);
        mShowDisabledNotification = getSettingsBoolean(mContentResolver,
                Settings.Secure.WIFI_WATCHDOG_SHOW_DISABLED_NETWORK_POPUP, true);
    }

    /**
     * Helper to return wait time left given a min interval and last run
     *
     * @param interval minimum wait interval
     * @param lastTime last time action was performed in
     *            SystemClock.elapsedRealtime(). Null if never.
     * @return non negative time to wait
     */
    private static long waitTime(long interval, Long lastTime) {
        if (lastTime == null)
            return 0;
        long wait = interval + lastTime - SystemClock.elapsedRealtime();
        return wait > 0 ? wait : 0;
    }

    private static String wifiInfoToStr(WifiInfo wifiInfo) {
        if (wifiInfo == null)
            return "null";
        return "(" + wifiInfo.getSSID() + ", " + wifiInfo.getBSSID() + ")";
    }

    /**
     * Uses {@link #mConnectionInfo}.
     */
    private void updateBssids() {
        String curSsid = mConnectionInfo.getSSID();
        List<ScanResult> results = mWifiManager.getScanResults();
        int oldNumBssids = mBssids.size();

        if (results == null) {
            if (DBG) {
                log("updateBssids: Got null scan results!");
            }
            return;
        }

        for (ScanResult result : results) {
            if (result == null || result.SSID == null) {
                if (DBG) {
                    log("Received invalid scan result: " + result);
                }
                continue;
            }
            if (curSsid.equals(result.SSID))
                mBssids.add(result.BSSID);
        }
    }

    private void resetWatchdogState() {
        if (DBG) {
            log("Resetting watchdog state...");
        }
        mConnectionInfo = null;
        mDisableAPNextFailure = false;
        mLastWalledGardenCheckTime = null;
        mNumCheckFailures = 0;
        mBssids.clear();
        setDisabledNetworkNotificationVisible(false);
        setWalledGardenNotificationVisible(false);
    }

    private void setWalledGardenNotificationVisible(boolean visible) {
        // If it should be hidden and it is already hidden, then noop
        if (!visible && !mWalledGardenNotificationShown) {
            return;
        }

        Resources r = Resources.getSystem();
        NotificationManager notificationManager = (NotificationManager) mContext
            .getSystemService(Context.NOTIFICATION_SERVICE);

        if (visible) {
            Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(mWalledGardenUrl));
            intent.setFlags(Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT | Intent.FLAG_ACTIVITY_NEW_TASK);

            CharSequence title = r.getString(R.string.wifi_available_sign_in, 0);
            CharSequence details = r.getString(R.string.wifi_available_sign_in_detailed,
                    mConnectionInfo.getSSID());

            Notification notification = new Notification();
            notification.when = 0;
            notification.icon = com.android.internal.R.drawable.stat_notify_wifi_in_range;
            notification.flags = Notification.FLAG_AUTO_CANCEL;
            notification.contentIntent = PendingIntent.getActivity(mContext, 0, intent, 0);
            notification.tickerText = title;
            notification.setLatestEventInfo(mContext, title, details, notification.contentIntent);

            notificationManager.notify(WALLED_GARDEN_NOTIFICATION_ID, 1, notification);
        } else {
            notificationManager.cancel(WALLED_GARDEN_NOTIFICATION_ID, 1);
        }
        mWalledGardenNotificationShown = visible;
    }

    private void setDisabledNetworkNotificationVisible(boolean visible) {
        // If it should be hidden and it is already hidden, then noop
        if (!visible && !mDisabledNotificationShown) {
            return;
        }

        Resources r = Resources.getSystem();
        NotificationManager notificationManager = (NotificationManager) mContext
            .getSystemService(Context.NOTIFICATION_SERVICE);

        if (visible) {
            CharSequence title = r.getText(R.string.wifi_watchdog_network_disabled);
            String msg = mConnectionInfo.getSSID() +
                r.getText(R.string.wifi_watchdog_network_disabled_detailed);

            Notification wifiDisabledWarning = new Notification.Builder(mContext)
                .setSmallIcon(R.drawable.stat_sys_warning)
                .setDefaults(Notification.DEFAULT_ALL)
                .setTicker(title)
                .setContentTitle(title)
                .setContentText(msg)
                .setContentIntent(PendingIntent.getActivity(mContext, 0,
                            new Intent(WifiManager.ACTION_PICK_WIFI_NETWORK)
                            .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK), 0))
                .setWhen(System.currentTimeMillis())
                .setAutoCancel(true)
                .getNotification();

            notificationManager.notify(DISABLED_NETWORK_NOTIFICATION_ID, 1, wifiDisabledWarning);
        } else {
            notificationManager.cancel(DISABLED_NETWORK_NOTIFICATION_ID, 1);
        }
        mDisabledNotificationShown = visible;
    }

    class DefaultState extends State {
        @Override
        public boolean processMessage(Message msg) {
            switch (msg.what) {
                case EVENT_WATCHDOG_SETTINGS_CHANGE:
                    updateSettings();
                    if (DBG) {
                        log("Updating wifi-watchdog secure settings");
                    }
                    return HANDLED;
            }
            if (DBG) {
                log("Caught message " + msg.what + " in state " +
                        getCurrentState().getName());
            }
            return HANDLED;
        }
    }

    class WatchdogDisabledState extends State {
        @Override
        public boolean processMessage(Message msg) {
            switch (msg.what) {
                case EVENT_WATCHDOG_TOGGLED:
                    if (isWatchdogEnabled())
                        transitionTo(mNotConnectedState);
                    return HANDLED;
            }
            return NOT_HANDLED;
        }
    }

    class WatchdogEnabledState extends State {
        @Override
        public void enter() {
            resetWatchdogState();
            mContext.registerReceiver(mBroadcastReceiver, mIntentFilter);
            if (DBG) log("WifiWatchdogService enabled");
        }

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

                    setDisabledNetworkNotificationVisible(false);
                    setWalledGardenNotificationVisible(false);
                    switch (networkInfo.getState()) {
                        case CONNECTED:
                            WifiInfo wifiInfo = (WifiInfo)
                                stateChangeIntent.getParcelableExtra(WifiManager.EXTRA_WIFI_INFO);
                            if (wifiInfo == null) {
                                loge("Connected --> WifiInfo object null!");
                                return HANDLED;
                            }

                            if (wifiInfo.getSSID() == null || wifiInfo.getBSSID() == null) {
                                loge("Received wifiInfo object with null elts: "
                                        + wifiInfoToStr(wifiInfo));
                                return HANDLED;
                            }

                            initConnection(wifiInfo);
                            mConnectionInfo = wifiInfo;
                            mNetEventCounter++;
                            if (mPoorNetworkDetectionEnabled) {
                                updateBssids();
                                transitionTo(mDnsCheckingState);
                            } else {
                                transitionTo(mDelayWalledGardenState);
                            }
                            break;
                        default:
                            mNetEventCounter++;
                            transitionTo(mNotConnectedState);
                            break;
                    }
                    return HANDLED;
                case EVENT_WIFI_RADIO_STATE_CHANGE:
                    if ((Integer) msg.obj == WifiManager.WIFI_STATE_DISABLING) {
                        if (DBG) log("WifiStateDisabling -- Resetting WatchdogState");
                        resetWatchdogState();
                        mNetEventCounter++;
                        transitionTo(mNotConnectedState);
                    }
                    return HANDLED;
            }

            return NOT_HANDLED;
        }

        /**
         * @param wifiInfo Info object with non-null ssid and bssid
         */
        private void initConnection(WifiInfo wifiInfo) {
            if (DBG) {
                log("Connected:: old " + wifiInfoToStr(mConnectionInfo) +
                        " ==> new " + wifiInfoToStr(wifiInfo));
            }

            if (mConnectionInfo == null || !wifiInfo.getSSID().equals(mConnectionInfo.getSSID())) {
                resetWatchdogState();
            } else if (!wifiInfo.getBSSID().equals(mConnectionInfo.getBSSID())) {
                mDisableAPNextFailure = false;
            }
        }

        @Override
        public void exit() {
            mContext.unregisterReceiver(mBroadcastReceiver);
            if (DBG) log("WifiWatchdogService disabled");
        }
    }

    class NotConnectedState extends State {
    }

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

    class DnsCheckingState extends State {
        List<InetAddress> mDnsList;
        int[] dnsCheckSuccesses;
        String dnsCheckLogStr;
        String[] dnsResponseStrs;
        /** Keeps track of active dns pings.  Map is from pingID to index in mDnsList */
        HashMap<Integer, Integer> idDnsMap = new HashMap<Integer, Integer>();

        @Override
        public void enter() {
            mDnsList = mDnsPinger.getDnsList();
            int numDnses = mDnsList.size();
            dnsCheckSuccesses = new int[numDnses];
            dnsResponseStrs = new String[numDnses];
            for (int i = 0; i < numDnses; i++)
                dnsResponseStrs[i] = "";

            if (DBG) {
                dnsCheckLogStr = String.format("Pinging %s on ssid [%s]: ",
                        mDnsList, mConnectionInfo.getSSID());
                log(dnsCheckLogStr);
            }

            idDnsMap.clear();
            for (int i=0; i < mNumDnsPings; i++) {
                for (int j = 0; j < numDnses; j++) {
                    idDnsMap.put(mDnsPinger.pingDnsAsync(mDnsList.get(j), mDnsPingTimeoutMs,
                            DNS_START_DELAY_MS + DNS_INTRATEST_PING_INTERVAL_MS * i), j);
                }
            }
        }

        @Override
        public boolean processMessage(Message msg) {
            if (msg.what != DnsPinger.DNS_PING_RESULT) {
                return NOT_HANDLED;
            }

            int pingID = msg.arg1;
            int pingResponseTime = msg.arg2;

            Integer dnsServerId = idDnsMap.get(pingID);
            if (dnsServerId == null) {
                loge("Received a Dns response with unknown ID!");
                return HANDLED;
            }

            idDnsMap.remove(pingID);
            if (pingResponseTime >= 0)
                dnsCheckSuccesses[dnsServerId]++;

            if (DBG) {
                if (pingResponseTime >= 0) {
                    dnsResponseStrs[dnsServerId] += "|" + pingResponseTime;
                } else {
                    dnsResponseStrs[dnsServerId] += "|x";
                }
            }

            /**
             * After a full ping count, if we have more responses than this
             * cutoff, the outcome is success; else it is 'failure'.
             */

            /**
             * Our final success count will be at least this big, so we're
             * guaranteed to succeed.
             */
            if (dnsCheckSuccesses[dnsServerId] >= mMinDnsResponses) {
                // DNS CHECKS OK, NOW WALLED GARDEN
                if (DBG) {
                    log(makeLogString() + "  SUCCESS");
                }

                if (!shouldCheckWalledGarden()) {
                    transitionTo(mOnlineWatchState);
                    return HANDLED;
                }

                transitionTo(mDelayWalledGardenState);
                return HANDLED;
            }

            if (idDnsMap.isEmpty()) {
                if (DBG) {
                    log(makeLogString() + "  FAILURE");
                }
                transitionTo(mDnsCheckFailureState);
                return HANDLED;
            }

            return HANDLED;
        }

        private String makeLogString() {
            String logStr = dnsCheckLogStr;
            for (String respStr : dnsResponseStrs)
                logStr += " [" + respStr + "]";
            return logStr;
        }

        @Override
        public void exit() {
            mDnsPinger.cancelPings();
        }

        private boolean shouldCheckWalledGarden() {
            if (!mWalledGardenTestEnabled) {
                if (DBG)
                    log("Skipping walled garden check - disabled");
                return false;
            }
            long waitTime = waitTime(mWalledGardenIntervalMs,
                    mLastWalledGardenCheckTime);
            if (waitTime > 0) {
                if (DBG) {
                    log("Skipping walled garden check - wait " +
                            waitTime + " ms.");
                }
                return false;
            }
            return true;
        }
    }

    class DelayWalledGardenState extends State {
        @Override
        public void enter() {
            sendMessageDelayed(MESSAGE_DELAYED_WALLED_GARDEN_CHECK, WALLED_GARDEN_START_DELAY_MS);
        }

        @Override
        public boolean processMessage(Message msg) {
            switch (msg.what) {
                case MESSAGE_DELAYED_WALLED_GARDEN_CHECK:
                    mLastWalledGardenCheckTime = SystemClock.elapsedRealtime();
                    if (isWalledGardenConnection()) {
                        if (DBG) log("Walled garden test complete - walled garden detected");
                        transitionTo(mWalledGardenState);
                    } else {
                        if (DBG) log("Walled garden test complete - online");
                        if (mPoorNetworkDetectionEnabled) {
                            transitionTo(mOnlineWatchState);
                        } else {
                            transitionTo(mOnlineState);
                        }
                    }
                    return HANDLED;
                default:
                    return NOT_HANDLED;
            }
        }
    }

    class OnlineWatchState extends State {
        /**
         * Signals a short-wait message is enqueued for the current 'guard' counter
         */
        boolean unstableSignalChecks = false;

        /**
         * The signal is unstable.  We should enqueue a short-wait check, if one is enqueued
         * already
         */
        boolean signalUnstable = false;

        /**
         * A monotonic counter to ensure that at most one check message will be processed from any
         * set of check messages currently enqueued.  Avoids duplicate checks when a low-signal
         * event is observed.
         */
        int checkGuard = 0;
        Long lastCheckTime = null;

        /** Keeps track of dns pings.  Map is from pingID to InetAddress used for ping */
        HashMap<Integer, InetAddress> pingInfoMap = new HashMap<Integer, InetAddress>();

        @Override
        public void enter() {
            lastCheckTime = SystemClock.elapsedRealtime();
            signalUnstable = false;
            checkGuard++;
            unstableSignalChecks = false;
            pingInfoMap.clear();
            triggerSingleDnsCheck();
        }

        @Override
        public boolean processMessage(Message msg) {
            switch (msg.what) {
                case EVENT_RSSI_CHANGE:
                    if (msg.arg1 != mNetEventCounter) {
                        if (DBG) {
                            log("Rssi change message out of sync, ignoring");
                        }
                        return HANDLED;
                    }
                    int newRssi = msg.arg2;
                    signalUnstable = !rssiStrengthAboveCutoff(newRssi);
                    if (DBG) {
                        log("OnlineWatchState:: new rssi " + newRssi + " --> level " +
                                WifiManager.calculateSignalLevel(newRssi, WIFI_SIGNAL_LEVELS));
                    }

                    if (signalUnstable && !unstableSignalChecks) {
                        if (DBG) {
                            log("Sending triggered check msg");
                        }
                        triggerSingleDnsCheck();
                    }
                    return HANDLED;
                case MESSAGE_SINGLE_DNS_CHECK:
                    if (msg.arg1 != checkGuard) {
                        if (DBG) {
                            log("Single check msg out of sync, ignoring.");
                        }
                        return HANDLED;
                    }
                    lastCheckTime = SystemClock.elapsedRealtime();
                    pingInfoMap.clear();
                    for (InetAddress curDns: mDnsPinger.getDnsList()) {
                        pingInfoMap.put(mDnsPinger.pingDnsAsync(curDns, mDnsPingTimeoutMs, 0),
                                curDns);
                    }
                    return HANDLED;
                case DnsPinger.DNS_PING_RESULT:
                    InetAddress curDnsServer = pingInfoMap.get(msg.arg1);
                    if (curDnsServer == null) {
                        return HANDLED;
                    }
                    pingInfoMap.remove(msg.arg1);
                    int responseTime = msg.arg2;
                    if (responseTime >= 0) {
                        if (DBG) {
                            log("Single DNS ping OK. Response time: "
                                    + responseTime + " from DNS " + curDnsServer);
                        }
                        pingInfoMap.clear();

                        checkGuard++;
                        unstableSignalChecks = false;
                        triggerSingleDnsCheck();
                    } else {
                        if (pingInfoMap.isEmpty()) {
                            if (DBG) {
                                log("Single dns ping failure. All dns servers failed, "
                                        + "starting full checks.");
                            }
                            transitionTo(mDnsCheckingState);
                        }
                    }
                    return HANDLED;
            }
            return NOT_HANDLED;
        }

        @Override
        public void exit() {
            mDnsPinger.cancelPings();
        }

        /**
         * Times a dns check with an interval based on {@link #signalUnstable}
         */
        private void triggerSingleDnsCheck() {
            long waitInterval;
            if (signalUnstable) {
                waitInterval = mDnsCheckShortIntervalMs;
                unstableSignalChecks = true;
            } else {
                waitInterval = mDnsCheckLongIntervalMs;
            }
            sendMessageDelayed(obtainMessage(MESSAGE_SINGLE_DNS_CHECK, checkGuard, 0),
                    waitTime(waitInterval, lastCheckTime));
        }
    }


    /* Child state of ConnectedState indicating that we are online
     * and there is nothing to do
     */
    class OnlineState extends State {
    }

    class DnsCheckFailureState extends State {

        @Override
        public void enter() {
            mNumCheckFailures++;
            obtainMessage(MESSAGE_HANDLE_BAD_AP, mNetEventCounter, 0).sendToTarget();
        }

        @Override
        public boolean processMessage(Message msg) {
            if (msg.what != MESSAGE_HANDLE_BAD_AP) {
                return NOT_HANDLED;
            }

            if (msg.arg1 != mNetEventCounter) {
                if (DBG) {
                    log("Msg out of sync, ignoring...");
                }
                return HANDLED;
            }

            if (mDisableAPNextFailure || mNumCheckFailures >= mBssids.size()
                    || mNumCheckFailures >= mMaxSsidBlacklists) {
                if (sWifiOnly) {
                    log("Would disable bad network, but device has no mobile data!" +
                            "  Going idle...");
                    // This state should be called idle -- will be changing flow.
                    transitionTo(mNotConnectedState);
                    return HANDLED;
                }

                // TODO : Unban networks if they had low signal ?
                log("Disabling current SSID " + wifiInfoToStr(mConnectionInfo)
                        + ".  " + "numCheckFailures " + mNumCheckFailures
                        + ", numAPs " + mBssids.size());
                int networkId = mConnectionInfo.getNetworkId();
                if (!mHasConnectedWifiManager) {
                    mWifiManager.asyncConnect(mContext, getHandler());
                    mHasConnectedWifiManager = true;
                }
                mWifiManager.disableNetwork(networkId, WifiConfiguration.DISABLED_DNS_FAILURE);
                if (mShowDisabledNotification && mConnectionInfo.isExplicitConnect()) {
                    setDisabledNetworkNotificationVisible(true);
                }
                transitionTo(mNotConnectedState);
            } else {
                log("Blacklisting current BSSID.  " + wifiInfoToStr(mConnectionInfo)
                       + "numCheckFailures " + mNumCheckFailures + ", numAPs " + mBssids.size());

                mWifiManager.addToBlacklist(mConnectionInfo.getBSSID());
                mWifiManager.reassociate();
                transitionTo(mBlacklistedApState);
            }
            return HANDLED;
        }
    }

    class WalledGardenState extends State {
        @Override
        public void enter() {
            obtainMessage(MESSAGE_HANDLE_WALLED_GARDEN, mNetEventCounter, 0).sendToTarget();
        }

        @Override
        public boolean processMessage(Message msg) {
            if (msg.what != MESSAGE_HANDLE_WALLED_GARDEN) {
                return NOT_HANDLED;
            }

            if (msg.arg1 != mNetEventCounter) {
                if (DBG) {
                    log("WalledGardenState::Msg out of sync, ignoring...");
                }
                return HANDLED;
            }
            setWalledGardenNotificationVisible(true);
            if (mPoorNetworkDetectionEnabled) {
                transitionTo(mOnlineWatchState);
            } else {
                transitionTo(mOnlineState);
            }
            return HANDLED;
        }
    }

    class BlacklistedApState extends State {
        @Override
        public void enter() {
            mDisableAPNextFailure = true;
            sendMessageDelayed(obtainMessage(MESSAGE_NETWORK_FOLLOWUP, mNetEventCounter, 0),
                    mBlacklistFollowupIntervalMs);
        }

        @Override
        public boolean processMessage(Message msg) {
            if (msg.what != MESSAGE_NETWORK_FOLLOWUP) {
                return NOT_HANDLED;
            }

            if (msg.arg1 != mNetEventCounter) {
                if (DBG) {
                    log("BlacklistedApState::Msg out of sync, ignoring...");
                }
                return HANDLED;
            }

            transitionTo(mDnsCheckingState);
            return HANDLED;
        }
    }


    /**
     * Convenience function for retrieving a single secure settings value
     * as a string with a default value.
     *
     * @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
     */
    private static String getSettingsStr(ContentResolver cr, String name, String def) {
        String v = Settings.Secure.getString(cr, name);
        return v != null ? v : def;
    }

    /**
     * Convenience function for retrieving a single secure settings value
     * as a boolean.  Note that internally setting values are always
     * stored as strings; this function converts the string to a boolean
     * for you.  The default value will be returned if the setting is
     * not defined or not a valid boolean.
     *
     * @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 getSettingsBoolean(ContentResolver cr, String name, boolean def) {
        return Settings.Secure.getInt(cr, name, def ? 1 : 0) == 1;
    }

    /**
     * Convenience function for updating a single settings value as an
     * integer. This will either create a new entry in the table if the
     * given name does not exist, or modify the value of the existing row
     * with that name.  Note that internally setting values are always
     * stored as strings, so this function converts the given value to a
     * string before storing it.
     *
     * @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 putSettingsBoolean(ContentResolver cr, String name, boolean value) {
        return Settings.Secure.putInt(cr, name, value ? 1 : 0);
    }

    private void log(String s) {
        Log.d(TAG, s);
    }

    private void loge(String s) {
        Log.e(TAG, s);
    }
}
