WifiWatchdogStateMachine.java revision 8dc6a1b2823f374a176fb21b8a174664a5f825fa
1/*
2 * Copyright (C) 2011 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package android.net.wifi;
18
19import android.app.Notification;
20import android.app.NotificationManager;
21import android.app.PendingIntent;
22import android.content.BroadcastReceiver;
23import android.content.ContentResolver;
24import android.content.Context;
25import android.content.Intent;
26import android.content.IntentFilter;
27import android.content.res.Resources;
28import android.database.ContentObserver;
29import android.net.ConnectivityManager;
30import android.net.DnsPinger;
31import android.net.NetworkInfo;
32import android.net.Uri;
33import android.os.Message;
34import android.os.SystemClock;
35import android.os.SystemProperties;
36import android.provider.Settings;
37import android.provider.Settings.Secure;
38import android.util.Slog;
39
40import com.android.internal.util.Protocol;
41import com.android.internal.util.State;
42import com.android.internal.util.StateMachine;
43
44import java.io.IOException;
45import java.io.PrintWriter;
46import java.net.HttpURLConnection;
47import java.net.InetAddress;
48import java.net.URL;
49import java.util.HashSet;
50import java.util.List;
51import java.util.Set;
52
53/**
54 * {@link WifiWatchdogStateMachine} monitors the initial connection to a Wi-Fi
55 * network with multiple access points. After the framework successfully
56 * connects to an access point, the watchdog verifies connectivity by 'pinging'
57 * the configured DNS server using {@link DnsPinger}.
58 * <p>
59 * On DNS check failure, the BSSID is blacklisted if it is reasonably likely
60 * that another AP might have internet access; otherwise the SSID is disabled.
61 * <p>
62 * On DNS success, the WatchdogService initiates a walled garden check via an
63 * http get. A browser window is activated if a walled garden is detected.
64 *
65 * @hide
66 */
67public class WifiWatchdogStateMachine extends StateMachine {
68
69
70    private static final boolean VDBG = false;
71    private static final boolean DBG = true;
72    private static final String WWSM_TAG = "WifiWatchdogStateMachine";
73    private static final String WATCHDOG_NOTIFICATION_ID = "Android.System.WifiWatchdog";
74
75    private static final int WIFI_SIGNAL_LEVELS = 4;
76    /**
77     * Low signal is defined as less than or equal to cut off
78     */
79    private static final int LOW_SIGNAL_CUTOFF = 1;
80
81    private static final long DEFAULT_DNS_CHECK_SHORT_INTERVAL_MS = 2 * 60 * 1000;
82    private static final long DEFAULT_DNS_CHECK_LONG_INTERVAL_MS = 10 * 60 * 1000;
83    private static final long DEFAULT_WALLED_GARDEN_INTERVAL_MS = 30 * 60 * 1000;
84
85    private static final int DEFAULT_MAX_SSID_BLACKLISTS = 7;
86    private static final int DEFAULT_NUM_DNS_PINGS = 5;
87    private static final int DEFAULT_MIN_DNS_RESPONSES = 3;
88
89    private static final int DEFAULT_DNS_PING_TIMEOUT_MS = 2000;
90
91    private static final long DEFAULT_BLACKLIST_FOLLOWUP_INTERVAL_MS = 15 * 1000;
92
93    // See http://go/clientsdns for usage approval
94    private static final String DEFAULT_WALLED_GARDEN_URL =
95            "http://clients3.google.com/generate_204";
96    private static final int WALLED_GARDEN_SOCKET_TIMEOUT_MS = 10000;
97    private static final int DNS_INTRATEST_PING_INTERVAL = 20;
98
99    private static final int BASE = Protocol.BASE_WIFI_WATCHDOG;
100
101    /**
102     * Indicates the enable setting of WWS may have changed
103     */
104    private static final int EVENT_WATCHDOG_TOGGLED = BASE + 1;
105
106    /**
107     * Indicates the wifi network state has changed. Passed w/ original intent
108     * which has a non-null networkInfo object
109     */
110    private static final int EVENT_NETWORK_STATE_CHANGE = BASE + 2;
111    /**
112     * Indicates the signal has changed. Passed with arg1
113     * {@link #mNetEventCounter} and arg2 [raw signal strength]
114     */
115    private static final int EVENT_RSSI_CHANGE = BASE + 3;
116    private static final int EVENT_SCAN_RESULTS_AVAILABLE = BASE + 4;
117    private static final int EVENT_WIFI_RADIO_STATE_CHANGE = BASE + 5;
118    private static final int EVENT_WATCHDOG_SETTINGS_CHANGE = BASE + 6;
119
120    private static final int MESSAGE_HANDLE_WALLED_GARDEN = BASE + 100;
121    private static final int MESSAGE_HANDLE_BAD_AP = BASE + 101;
122    /**
123     * arg1 == mOnlineWatchState.checkCount
124     */
125    private static final int MESSAGE_SINGLE_DNS_CHECK = BASE + 103;
126    private static final int MESSAGE_NETWORK_FOLLOWUP = BASE + 104;
127
128    private Context mContext;
129    private ContentResolver mContentResolver;
130    private WifiManager mWifiManager;
131    private DnsPinger mDnsPinger;
132    private IntentFilter mIntentFilter;
133    private BroadcastReceiver mBroadcastReceiver;
134
135    private DefaultState mDefaultState = new DefaultState();
136    private WatchdogDisabledState mWatchdogDisabledState = new WatchdogDisabledState();
137    private WatchdogEnabledState mWatchdogEnabledState = new WatchdogEnabledState();
138    private NotConnectedState mNotConnectedState = new NotConnectedState();
139    private ConnectedState mConnectedState = new ConnectedState();
140    private DnsCheckingState mDnsCheckingState = new DnsCheckingState();
141    private OnlineWatchState mOnlineWatchState = new OnlineWatchState();
142    private DnsCheckFailureState mDnsCheckFailureState = new DnsCheckFailureState();
143    private WalledGardenState mWalledGardenState = new WalledGardenState();
144    private BlacklistedApState mBlacklistedApState = new BlacklistedApState();
145
146    private long mDnsCheckShortIntervalMs;
147    private long mDnsCheckLongIntervalMs;
148    private long mWalledGardenIntervalMs;
149    private int mMaxSsidBlacklists;
150    private int mNumDnsPings;
151    private int mMinDnsResponses;
152    private int mDnsPingTimeoutMs;
153    private long mBlacklistFollowupIntervalMs;
154    private boolean mWalledGardenTestEnabled;
155    private String mWalledGardenUrl;
156
157    private boolean mShowDisabledNotification;
158    /**
159     * The {@link WifiInfo} object passed to WWSM on network broadcasts
160     */
161    private WifiInfo mConnectionInfo;
162    private int mNetEventCounter = 0;
163
164    /**
165     * Currently maintained but not used, TODO
166     */
167    private HashSet<String> mBssids = new HashSet<String>();
168    private int mNumCheckFailures = 0;
169
170    private Long mLastWalledGardenCheckTime = null;
171
172    /**
173     * This is set by the blacklisted state and reset when connected to a new AP.
174     * It triggers a disableNetwork call if a DNS check fails.
175     */
176    public boolean mDisableAPNextFailure = false;
177    private ConnectivityManager mConnectivityManager;
178    private boolean mNotificationShown;
179    public boolean mHasConnectedWifiManager = false;
180
181    /**
182     * STATE MAP
183     *          Default
184     *         /       \
185     * Disabled     Enabled
186     *             /       \
187     * NotConnected      Connected
188     *                  /---------\
189     *               (all other states)
190     */
191    private WifiWatchdogStateMachine(Context context) {
192        super(WWSM_TAG);
193        mContext = context;
194        mContentResolver = context.getContentResolver();
195        mWifiManager = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
196        mDnsPinger = new DnsPinger(mContext, "WifiWatchdogStateMachine.DnsPinger",
197                                this.getHandler().getLooper(), this.getHandler(),
198                                ConnectivityManager.TYPE_WIFI);
199
200        setupNetworkReceiver();
201
202        // The content observer to listen needs a handler
203        registerForSettingsChanges();
204        registerForWatchdogToggle();
205        addState(mDefaultState);
206            addState(mWatchdogDisabledState, mDefaultState);
207            addState(mWatchdogEnabledState, mDefaultState);
208                addState(mNotConnectedState, mWatchdogEnabledState);
209                addState(mConnectedState, mWatchdogEnabledState);
210                    addState(mDnsCheckingState, mConnectedState);
211                    addState(mDnsCheckFailureState, mConnectedState);
212                    addState(mWalledGardenState, mConnectedState);
213                    addState(mBlacklistedApState, mConnectedState);
214                    addState(mOnlineWatchState, mConnectedState);
215
216        setInitialState(mWatchdogDisabledState);
217        updateSettings();
218    }
219
220    public static WifiWatchdogStateMachine makeWifiWatchdogStateMachine(Context context) {
221        ContentResolver contentResolver = context.getContentResolver();
222        // Disable for wifi only devices.
223        if (Settings.Secure.getString(contentResolver, Settings.Secure.WIFI_WATCHDOG_ON) == null &&
224                "wifi-only".equals(SystemProperties.get("ro.carrier"))) {
225            putSettingsBoolean(contentResolver, Settings.Secure.WIFI_WATCHDOG_ON, false);
226        }
227        WifiWatchdogStateMachine wwsm = new WifiWatchdogStateMachine(context);
228        wwsm.start();
229        wwsm.sendMessage(EVENT_WATCHDOG_TOGGLED);
230        return wwsm;
231    }
232
233    /**
234   *
235   */
236    private void setupNetworkReceiver() {
237        mBroadcastReceiver = new BroadcastReceiver() {
238            @Override
239            public void onReceive(Context context, Intent intent) {
240                String action = intent.getAction();
241                if (action.equals(WifiManager.NETWORK_STATE_CHANGED_ACTION)) {
242                    sendMessage(EVENT_NETWORK_STATE_CHANGE, intent);
243                } else if (action.equals(WifiManager.RSSI_CHANGED_ACTION)) {
244                    obtainMessage(EVENT_RSSI_CHANGE, mNetEventCounter,
245                            intent.getIntExtra(WifiManager.EXTRA_NEW_RSSI, -200)).sendToTarget();
246                } else if (action.equals(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION)) {
247                    sendMessage(EVENT_SCAN_RESULTS_AVAILABLE);
248                } else if (action.equals(WifiManager.WIFI_STATE_CHANGED_ACTION)) {
249                    sendMessage(EVENT_WIFI_RADIO_STATE_CHANGE,
250                            intent.getIntExtra(WifiManager.EXTRA_WIFI_STATE,
251                                    WifiManager.WIFI_STATE_UNKNOWN));
252                }
253            }
254        };
255
256        mIntentFilter = new IntentFilter();
257        mIntentFilter.addAction(WifiManager.NETWORK_STATE_CHANGED_ACTION);
258        mIntentFilter.addAction(WifiManager.WIFI_STATE_CHANGED_ACTION);
259        mIntentFilter.addAction(WifiManager.RSSI_CHANGED_ACTION);
260        mIntentFilter.addAction(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION);
261    }
262
263    /**
264     * Observes the watchdog on/off setting, and takes action when changed.
265     */
266    private void registerForWatchdogToggle() {
267        ContentObserver contentObserver = new ContentObserver(this.getHandler()) {
268            @Override
269            public void onChange(boolean selfChange) {
270                sendMessage(EVENT_WATCHDOG_TOGGLED);
271            }
272        };
273
274        mContext.getContentResolver().registerContentObserver(
275                Settings.Secure.getUriFor(Settings.Secure.WIFI_WATCHDOG_ON),
276                false, contentObserver);
277    }
278
279    /**
280     * Observes watchdogs secure setting changes.
281     */
282    private void registerForSettingsChanges() {
283        ContentObserver contentObserver = new ContentObserver(this.getHandler()) {
284            @Override
285            public void onChange(boolean selfChange) {
286                sendMessage(EVENT_WATCHDOG_SETTINGS_CHANGE);
287            }
288        };
289
290        mContext.getContentResolver().registerContentObserver(
291                Settings.Secure.getUriFor(
292                        Settings.Secure.WIFI_WATCHDOG_DNS_CHECK_SHORT_INTERVAL_MS),
293                        false, contentObserver);
294        mContext.getContentResolver().registerContentObserver(
295                Settings.Secure.getUriFor(Settings.Secure.WIFI_WATCHDOG_DNS_CHECK_LONG_INTERVAL_MS),
296                false, contentObserver);
297        mContext.getContentResolver().registerContentObserver(
298                Settings.Secure.getUriFor(Settings.Secure.WIFI_WATCHDOG_WALLED_GARDEN_INTERVAL_MS),
299                false, contentObserver);
300        mContext.getContentResolver().registerContentObserver(
301                Settings.Secure.getUriFor(Settings.Secure.WIFI_WATCHDOG_MAX_SSID_BLACKLISTS),
302                false, contentObserver);
303        mContext.getContentResolver().registerContentObserver(
304                Settings.Secure.getUriFor(Settings.Secure.WIFI_WATCHDOG_NUM_DNS_PINGS),
305                false, contentObserver);
306        mContext.getContentResolver().registerContentObserver(
307                Settings.Secure.getUriFor(Settings.Secure.WIFI_WATCHDOG_MIN_DNS_RESPONSES),
308                false, contentObserver);
309        mContext.getContentResolver().registerContentObserver(
310                Settings.Secure.getUriFor(Settings.Secure.WIFI_WATCHDOG_DNS_PING_TIMEOUT_MS),
311                false, contentObserver);
312        mContext.getContentResolver().registerContentObserver(
313                Settings.Secure.getUriFor(
314                        Settings.Secure.WIFI_WATCHDOG_BLACKLIST_FOLLOWUP_INTERVAL_MS),
315                        false, contentObserver);
316        mContext.getContentResolver().registerContentObserver(
317                Settings.Secure.getUriFor(Settings.Secure.WIFI_WATCHDOG_WALLED_GARDEN_TEST_ENABLED),
318                false, contentObserver);
319        mContext.getContentResolver().registerContentObserver(
320                Settings.Secure.getUriFor(Settings.Secure.WIFI_WATCHDOG_WALLED_GARDEN_URL),
321                false, contentObserver);
322        mContext.getContentResolver().registerContentObserver(
323                Settings.Secure.getUriFor(Settings.Secure.WIFI_WATCHDOG_SHOW_DISABLED_NETWORK_POPUP)
324                , false, contentObserver);
325    }
326
327    /**
328     * DNS based detection techniques do not work at all hotspots. The one sure
329     * way to check a walled garden is to see if a URL fetch on a known address
330     * fetches the data we expect
331     */
332    private boolean isWalledGardenConnection() {
333        HttpURLConnection urlConnection = null;
334        try {
335            URL url = new URL(mWalledGardenUrl);
336            urlConnection = (HttpURLConnection) url.openConnection();
337            urlConnection.setInstanceFollowRedirects(false);
338            urlConnection.setConnectTimeout(WALLED_GARDEN_SOCKET_TIMEOUT_MS);
339            urlConnection.setReadTimeout(WALLED_GARDEN_SOCKET_TIMEOUT_MS);
340            urlConnection.setUseCaches(false);
341            urlConnection.getInputStream();
342            // We got a valid response, but not from the real google
343            return urlConnection.getResponseCode() != 204;
344        } catch (IOException e) {
345            if (DBG) {
346                Slog.d(WWSM_TAG, "Walled garden check - probably not a portal: exception ", e);
347            }
348            return false;
349        } finally {
350            if (urlConnection != null) {
351                urlConnection.disconnect();
352            }
353        }
354    }
355
356    private boolean rssiStrengthAboveCutoff(int rssi) {
357        return WifiManager.calculateSignalLevel(rssi, WIFI_SIGNAL_LEVELS) > LOW_SIGNAL_CUTOFF;
358    }
359
360    public void dump(PrintWriter pw) {
361        pw.print("WatchdogStatus: ");
362        pw.print("State " + getCurrentState());
363        pw.println(", network [" + mConnectionInfo + "]");
364        pw.print("checkFailures   " + mNumCheckFailures);
365        pw.println(", bssids: " + mBssids);
366        pw.println("lastSingleCheck: " + mOnlineWatchState.lastCheckTime);
367    }
368
369    private boolean isWatchdogEnabled() {
370        return getSettingsBoolean(mContentResolver, Settings.Secure.WIFI_WATCHDOG_ON, true);
371    }
372
373    private void updateSettings() {
374        mDnsCheckShortIntervalMs = Secure.getLong(mContentResolver,
375                Secure.WIFI_WATCHDOG_DNS_CHECK_SHORT_INTERVAL_MS,
376                DEFAULT_DNS_CHECK_SHORT_INTERVAL_MS);
377        mDnsCheckLongIntervalMs = Secure.getLong(mContentResolver,
378                Secure.WIFI_WATCHDOG_DNS_CHECK_LONG_INTERVAL_MS,
379                DEFAULT_DNS_CHECK_LONG_INTERVAL_MS);
380        mMaxSsidBlacklists = Secure.getInt(mContentResolver,
381                Secure.WIFI_WATCHDOG_MAX_SSID_BLACKLISTS,
382                DEFAULT_MAX_SSID_BLACKLISTS);
383        mNumDnsPings = Secure.getInt(mContentResolver,
384                Secure.WIFI_WATCHDOG_NUM_DNS_PINGS,
385                DEFAULT_NUM_DNS_PINGS);
386        mMinDnsResponses = Secure.getInt(mContentResolver,
387                Secure.WIFI_WATCHDOG_MIN_DNS_RESPONSES,
388                DEFAULT_MIN_DNS_RESPONSES);
389        mDnsPingTimeoutMs = Secure.getInt(mContentResolver,
390                Secure.WIFI_WATCHDOG_DNS_PING_TIMEOUT_MS,
391                DEFAULT_DNS_PING_TIMEOUT_MS);
392        mBlacklistFollowupIntervalMs = Secure.getLong(mContentResolver,
393                Settings.Secure.WIFI_WATCHDOG_BLACKLIST_FOLLOWUP_INTERVAL_MS,
394                DEFAULT_BLACKLIST_FOLLOWUP_INTERVAL_MS);
395        mWalledGardenTestEnabled = getSettingsBoolean(mContentResolver,
396                Settings.Secure.WIFI_WATCHDOG_WALLED_GARDEN_TEST_ENABLED, true);
397        mWalledGardenUrl = getSettingsStr(mContentResolver,
398                Settings.Secure.WIFI_WATCHDOG_WALLED_GARDEN_URL,
399                DEFAULT_WALLED_GARDEN_URL);
400        mWalledGardenIntervalMs = Secure.getLong(mContentResolver,
401                Secure.WIFI_WATCHDOG_WALLED_GARDEN_INTERVAL_MS,
402                DEFAULT_WALLED_GARDEN_INTERVAL_MS);
403        mShowDisabledNotification = getSettingsBoolean(mContentResolver,
404                Settings.Secure.WIFI_WATCHDOG_SHOW_DISABLED_NETWORK_POPUP, true);
405    }
406
407    /**
408     * Helper to return wait time left given a min interval and last run
409     *
410     * @param interval minimum wait interval
411     * @param lastTime last time action was performed in
412     *            SystemClock.elapsedRealtime(). Null if never.
413     * @return non negative time to wait
414     */
415    private static long waitTime(long interval, Long lastTime) {
416        if (lastTime == null)
417            return 0;
418        long wait = interval + lastTime - SystemClock.elapsedRealtime();
419        return wait > 0 ? wait : 0;
420    }
421
422    private static String wifiInfoToStr(WifiInfo wifiInfo) {
423        if (wifiInfo == null)
424            return "null";
425        return "(" + wifiInfo.getSSID() + ", " + wifiInfo.getBSSID() + ")";
426    }
427
428    /**
429     * Uses {@link #mConnectionInfo}.
430     */
431    private void updateBssids() {
432        String curSsid = mConnectionInfo.getSSID();
433        List<ScanResult> results = mWifiManager.getScanResults();
434        int oldNumBssids = mBssids.size();
435
436        if (results == null) {
437            if (DBG) {
438                Slog.d(WWSM_TAG, "updateBssids: Got null scan results!");
439            }
440            return;
441        }
442
443        for (ScanResult result : results) {
444            if (result == null || result.SSID == null) {
445                if (DBG) {
446                    Slog.d(WWSM_TAG, "Received invalid scan result: " + result);
447                }
448                continue;
449            }
450            if (curSsid.equals(result.SSID))
451                mBssids.add(result.BSSID);
452        }
453    }
454
455    private void resetWatchdogState() {
456        if (VDBG) {
457            Slog.v(WWSM_TAG, "Resetting watchdog state...");
458        }
459        mConnectionInfo = null;
460        mDisableAPNextFailure = false;
461        mLastWalledGardenCheckTime = null;
462        mNumCheckFailures = 0;
463        mBssids.clear();
464        cancelNetworkNotification();
465    }
466
467    private void popUpBrowser() {
468        Uri uri = Uri.parse("http://www.google.com");
469        Intent intent = new Intent(Intent.ACTION_VIEW, uri);
470        intent.setFlags(Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT |
471                Intent.FLAG_ACTIVITY_NEW_TASK);
472        mContext.startActivity(intent);
473    }
474
475    private void displayDisabledNetworkNotification(String ssid) {
476        Resources r = Resources.getSystem();
477        CharSequence title =
478                r.getText(com.android.internal.R.string.wifi_watchdog_network_disabled);
479        String msg = ssid +
480                r.getText(com.android.internal.R.string.wifi_watchdog_network_disabled_detailed);
481
482        Notification wifiDisabledWarning = new Notification.Builder(mContext)
483            .setSmallIcon(com.android.internal.R.drawable.stat_sys_warning)
484            .setDefaults(Notification.DEFAULT_ALL)
485            .setTicker(title)
486            .setContentTitle(title)
487            .setContentText(msg)
488            .setContentIntent(PendingIntent.getActivity(mContext, 0,
489                    new Intent(WifiManager.ACTION_PICK_WIFI_NETWORK)
490                        .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK), 0))
491            .setWhen(System.currentTimeMillis())
492            .setAutoCancel(true)
493            .getNotification();
494
495        NotificationManager notificationManager = (NotificationManager) mContext
496                .getSystemService(Context.NOTIFICATION_SERVICE);
497
498        notificationManager.notify(WATCHDOG_NOTIFICATION_ID, 1, wifiDisabledWarning);
499        mNotificationShown = true;
500    }
501
502    public void cancelNetworkNotification() {
503        if (mNotificationShown) {
504            NotificationManager notificationManager = (NotificationManager) mContext
505                    .getSystemService(Context.NOTIFICATION_SERVICE);
506            notificationManager.cancel(WATCHDOG_NOTIFICATION_ID, 1);
507            mNotificationShown = false;
508        }
509    }
510
511    /**
512     * @return true if there is definitely no mobile data (we'll be less aggressive)
513     */
514    private boolean hasNoMobileData() {
515        if (mConnectivityManager == null) {
516            mConnectivityManager = (ConnectivityManager) mContext.getSystemService(
517                    Context.CONNECTIVITY_SERVICE);
518        }
519        NetworkInfo mobileNetInfo = mConnectivityManager.getNetworkInfo(
520                ConnectivityManager.TYPE_MOBILE);
521        if (mobileNetInfo == null || !mobileNetInfo.isAvailable()) {
522            return true;
523        }
524        return false;
525    }
526
527    class DefaultState extends State {
528        @Override
529        public boolean processMessage(Message msg) {
530            switch (msg.what) {
531                case EVENT_WATCHDOG_SETTINGS_CHANGE:
532                    updateSettings();
533                    if (VDBG) {
534                        Slog.d(WWSM_TAG, "Updating wifi-watchdog secure settings");
535                    }
536                    return HANDLED;
537            }
538            if (VDBG) {
539                Slog.v(WWSM_TAG, "Caught message " + msg.what + " in state " +
540                        getCurrentState().getName());
541            }
542            return HANDLED;
543        }
544    }
545
546    class WatchdogDisabledState extends State {
547        @Override
548        public boolean processMessage(Message msg) {
549            switch (msg.what) {
550                case EVENT_WATCHDOG_TOGGLED:
551                    if (isWatchdogEnabled())
552                        transitionTo(mNotConnectedState);
553                    return HANDLED;
554            }
555            return NOT_HANDLED;
556        }
557    }
558
559    class WatchdogEnabledState extends State {
560        @Override
561        public void enter() {
562            resetWatchdogState();
563            mContext.registerReceiver(mBroadcastReceiver, mIntentFilter);
564            Slog.i(WWSM_TAG, "WifiWatchdogService enabled");
565        }
566
567        @Override
568        public boolean processMessage(Message msg) {
569            switch (msg.what) {
570                case EVENT_WATCHDOG_TOGGLED:
571                    if (!isWatchdogEnabled())
572                        transitionTo(mWatchdogDisabledState);
573                    return HANDLED;
574                case EVENT_NETWORK_STATE_CHANGE:
575                    Intent stateChangeIntent = (Intent) msg.obj;
576                    NetworkInfo networkInfo = (NetworkInfo)
577                            stateChangeIntent.getParcelableExtra(WifiManager.EXTRA_NETWORK_INFO);
578
579                    switch (networkInfo.getState()) {
580                        case CONNECTED:
581                            cancelNetworkNotification();
582                            WifiInfo wifiInfo = (WifiInfo)
583                                stateChangeIntent.getParcelableExtra(WifiManager.EXTRA_WIFI_INFO);
584                            if (wifiInfo == null) {
585                                Slog.e(WWSM_TAG, "Connected --> WifiInfo object null!");
586                                return HANDLED;
587                            }
588
589                            if (wifiInfo.getSSID() == null || wifiInfo.getBSSID() == null) {
590                                Slog.e(WWSM_TAG, "Received wifiInfo object with null elts: "
591                                        + wifiInfoToStr(wifiInfo));
592                                return HANDLED;
593                            }
594
595                            initConnection(wifiInfo);
596                            mConnectionInfo = wifiInfo;
597                            updateBssids();
598                            transitionTo(mDnsCheckingState);
599                            mNetEventCounter++;
600                            return HANDLED;
601                        case DISCONNECTED:
602                        case DISCONNECTING:
603                            mNetEventCounter++;
604                            transitionTo(mNotConnectedState);
605                            return HANDLED;
606                    }
607                    return HANDLED;
608                case EVENT_WIFI_RADIO_STATE_CHANGE:
609                    if ((Integer) msg.obj == WifiManager.WIFI_STATE_DISABLING) {
610                        Slog.i(WWSM_TAG, "WifiStateDisabling -- Resetting WatchdogState");
611                        resetWatchdogState();
612                        mNetEventCounter++;
613                        transitionTo(mNotConnectedState);
614                    }
615                    return HANDLED;
616            }
617
618            return NOT_HANDLED;
619        }
620
621        /**
622         * @param wifiInfo Info object with non-null ssid and bssid
623         */
624        private void initConnection(WifiInfo wifiInfo) {
625            if (VDBG) {
626                Slog.v(WWSM_TAG, "Connected:: old " + wifiInfoToStr(mConnectionInfo) +
627                        " ==> new " + wifiInfoToStr(wifiInfo));
628            }
629
630            if (mConnectionInfo == null || !wifiInfo.getSSID().equals(mConnectionInfo.getSSID())) {
631                resetWatchdogState();
632            } else if (!wifiInfo.getBSSID().equals(mConnectionInfo.getBSSID())) {
633                mDisableAPNextFailure = false;
634            }
635        }
636
637        @Override
638        public void exit() {
639            mContext.unregisterReceiver(mBroadcastReceiver);
640            Slog.i(WWSM_TAG, "WifiWatchdogService disabled");
641        }
642    }
643
644    class NotConnectedState extends State {
645    }
646
647    class ConnectedState extends State {
648        @Override
649        public boolean processMessage(Message msg) {
650            switch (msg.what) {
651                case EVENT_SCAN_RESULTS_AVAILABLE:
652                    updateBssids();
653                    return HANDLED;
654                case EVENT_WATCHDOG_SETTINGS_CHANGE:
655                    // Stop current checks, but let state update
656                    transitionTo(mOnlineWatchState);
657                    return NOT_HANDLED;
658            }
659            return NOT_HANDLED;
660        }
661    }
662
663    class DnsCheckingState extends State {
664        int dnsCheckSuccesses = 0;
665        int dnsCheckTries = 0;
666        String dnsCheckLogStr = "";
667        Set<Integer> ids = new HashSet<Integer>();
668
669        @Override
670        public void enter() {
671            dnsCheckSuccesses = 0;
672            dnsCheckTries = 0;
673            ids.clear();
674            InetAddress dns = mDnsPinger.getDns();
675            if (DBG) {
676                Slog.d(WWSM_TAG, "Starting DNS pings at " + SystemClock.elapsedRealtime());
677                dnsCheckLogStr = String.format("Pinging %s on ssid [%s]: ",
678                        mDnsPinger.getDns(), mConnectionInfo.getSSID());
679            }
680
681            for (int i=0; i < mNumDnsPings; i++) {
682                ids.add(mDnsPinger.pingDnsAsync(dns, mDnsPingTimeoutMs,
683                        DNS_INTRATEST_PING_INTERVAL * i));
684            }
685        }
686
687        @Override
688        public boolean processMessage(Message msg) {
689            if (msg.what != DnsPinger.DNS_PING_RESULT) {
690                return NOT_HANDLED;
691            }
692
693            int pingID = msg.arg1;
694            int pingResponseTime = msg.arg2;
695
696            if (!ids.contains(pingID)) {
697                Slog.w(WWSM_TAG, "Received a Dns response with unknown ID!");
698                return HANDLED;
699            }
700            ids.remove(pingID);
701            dnsCheckTries++;
702            if (pingResponseTime >= 0)
703                dnsCheckSuccesses++;
704
705            if (DBG) {
706                if (pingResponseTime >= 0) {
707                    dnsCheckLogStr += "|" + pingResponseTime;
708                } else {
709                    dnsCheckLogStr += "|x";
710                }
711            }
712
713            if (VDBG) {
714                Slog.v(WWSM_TAG, dnsCheckLogStr);
715            }
716
717            /**
718             * After a full ping count, if we have more responses than this
719             * cutoff, the outcome is success; else it is 'failure'.
720             */
721
722            /**
723             * Our final success count will be at least this big, so we're
724             * guaranteed to succeed.
725             */
726            if (dnsCheckSuccesses >= mMinDnsResponses) {
727                // DNS CHECKS OK, NOW WALLED GARDEN
728                if (DBG) {
729                    Slog.d(WWSM_TAG, dnsCheckLogStr + "|  SUCCESS");
730                }
731
732                if (!shouldCheckWalledGarden()) {
733                    transitionTo(mOnlineWatchState);
734                    return HANDLED;
735                }
736
737                mLastWalledGardenCheckTime = SystemClock.elapsedRealtime();
738                if (isWalledGardenConnection()) {
739                    if (DBG)
740                        Slog.d(WWSM_TAG,
741                                "Walled garden test complete - walled garden detected");
742                    transitionTo(mWalledGardenState);
743                } else {
744                    if (DBG)
745                        Slog.d(WWSM_TAG, "Walled garden test complete - online");
746                    transitionTo(mOnlineWatchState);
747                }
748                return HANDLED;
749            }
750
751            /**
752             * Our final count will be at most the current count plus the
753             * remaining pings - we're guaranteed to fail.
754             */
755            int remainingChecks = mNumDnsPings - dnsCheckTries;
756            if (remainingChecks + dnsCheckSuccesses < mMinDnsResponses) {
757                if (DBG) {
758                    Slog.d(WWSM_TAG, dnsCheckLogStr + "|  FAILURE");
759                }
760                transitionTo(mDnsCheckFailureState);
761                return HANDLED;
762            }
763
764            return HANDLED;
765        }
766
767        @Override
768        public void exit() {
769            mDnsPinger.cancelPings();
770        }
771
772
773        private boolean shouldCheckWalledGarden() {
774            if (!mWalledGardenTestEnabled) {
775                if (VDBG)
776                    Slog.v(WWSM_TAG, "Skipping walled garden check - disabled");
777                return false;
778            }
779            long waitTime = waitTime(mWalledGardenIntervalMs,
780                    mLastWalledGardenCheckTime);
781            if (waitTime > 0) {
782                if (DBG) {
783                    Slog.d(WWSM_TAG, "Skipping walled garden check - wait " +
784                            waitTime + " ms.");
785                }
786                return false;
787            }
788            return true;
789        }
790    }
791
792    class OnlineWatchState extends State {
793        /**
794         * Signals a short-wait message is enqueued for the current 'guard' counter
795         */
796        boolean unstableSignalChecks = false;
797
798        /**
799         * The signal is unstable.  We should enqueue a short-wait check, if one is enqueued
800         * already
801         */
802        boolean signalUnstable = false;
803
804        /**
805         * A monotonic counter to ensure that at most one check message will be processed from any
806         * set of check messages currently enqueued.  Avoids duplicate checks when a low-signal
807         * event is observed.
808         */
809        int checkGuard = 0;
810        Long lastCheckTime = null;
811
812        int curPingID = 0;
813
814        @Override
815        public void enter() {
816            lastCheckTime = SystemClock.elapsedRealtime();
817            signalUnstable = false;
818            checkGuard++;
819            unstableSignalChecks = false;
820            curPingID = 0;
821            triggerSingleDnsCheck();
822        }
823
824        @Override
825        public boolean processMessage(Message msg) {
826            switch (msg.what) {
827                case EVENT_RSSI_CHANGE:
828                    if (msg.arg1 != mNetEventCounter) {
829                        if (DBG) {
830                            Slog.d(WWSM_TAG, "Rssi change message out of sync, ignoring");
831                        }
832                        return HANDLED;
833                    }
834                    int newRssi = msg.arg2;
835                    signalUnstable = !rssiStrengthAboveCutoff(newRssi);
836                    if (VDBG) {
837                        Slog.v(WWSM_TAG, "OnlineWatchState:: new rssi " + newRssi + " --> level " +
838                                WifiManager.calculateSignalLevel(newRssi, WIFI_SIGNAL_LEVELS));
839                    }
840
841                    if (signalUnstable && !unstableSignalChecks) {
842                        if (VDBG) {
843                            Slog.v(WWSM_TAG, "Sending triggered check msg");
844                        }
845                        triggerSingleDnsCheck();
846                    }
847                    return HANDLED;
848                case MESSAGE_SINGLE_DNS_CHECK:
849                    if (msg.arg1 != checkGuard) {
850                        if (VDBG) {
851                            Slog.v(WWSM_TAG, "Single check msg out of sync, ignoring.");
852                        }
853                        return HANDLED;
854                    }
855                    lastCheckTime = SystemClock.elapsedRealtime();
856                    curPingID = mDnsPinger.pingDnsAsync(mDnsPinger.getDns(),
857                            mDnsPingTimeoutMs, 0);
858                    return HANDLED;
859                case DnsPinger.DNS_PING_RESULT:
860                    if ((short) msg.arg1 != curPingID) {
861                        if (VDBG) {
862                            Slog.v(WWSM_TAG, "Received non-matching DnsPing w/ id: " +
863                                    msg.arg1);
864                        }
865                        return HANDLED;
866                    }
867                    int responseTime = msg.arg2;
868                    if (responseTime >= 0) {
869                        if (VDBG) {
870                            Slog.v(WWSM_TAG, "Ran a single DNS ping. Response time: "
871                                    + responseTime);
872                        }
873
874                        checkGuard++;
875                        unstableSignalChecks = false;
876                        triggerSingleDnsCheck();
877                    } else {
878                        if (DBG) {
879                            Slog.d(WWSM_TAG, "Single dns ping failure. Starting full checks.");
880                        }
881                        transitionTo(mDnsCheckingState);
882                    }
883                    return HANDLED;
884            }
885            return NOT_HANDLED;
886        }
887
888        @Override
889        public void exit() {
890            mDnsPinger.cancelPings();
891        }
892
893        /**
894         * Times a dns check with an interval based on {@link #signalUnstable}
895         */
896        private void triggerSingleDnsCheck() {
897            long waitInterval;
898            if (signalUnstable) {
899                waitInterval = mDnsCheckShortIntervalMs;
900                unstableSignalChecks = true;
901            } else {
902                waitInterval = mDnsCheckLongIntervalMs;
903            }
904            sendMessageDelayed(obtainMessage(MESSAGE_SINGLE_DNS_CHECK, checkGuard, 0),
905                    waitTime(waitInterval, lastCheckTime));
906        }
907    }
908
909    class DnsCheckFailureState extends State {
910
911        @Override
912        public void enter() {
913            mNumCheckFailures++;
914            obtainMessage(MESSAGE_HANDLE_BAD_AP, mNetEventCounter, 0).sendToTarget();
915        }
916
917        @Override
918        public boolean processMessage(Message msg) {
919            if (msg.what != MESSAGE_HANDLE_BAD_AP) {
920                return NOT_HANDLED;
921            }
922
923            if (msg.arg1 != mNetEventCounter) {
924                if (VDBG) {
925                    Slog.v(WWSM_TAG, "Msg out of sync, ignoring...");
926                }
927                return HANDLED;
928            }
929
930            if (mDisableAPNextFailure || mNumCheckFailures >= mBssids.size()
931                    || mNumCheckFailures >= mMaxSsidBlacklists) {
932                if (hasNoMobileData()) {
933                    Slog.w(WWSM_TAG, "Would disable bad network, but device has no mobile data!" +
934                            "  Going idle...");
935                    // This state should be called idle -- will be changing flow.
936                    transitionTo(mNotConnectedState);
937                    return HANDLED;
938                }
939
940                // TODO : Unban networks if they had low signal ?
941                Slog.i(WWSM_TAG, "Disabling current SSID " + wifiInfoToStr(mConnectionInfo)
942                        + ".  " + "numCheckFailures " + mNumCheckFailures
943                        + ", numAPs " + mBssids.size());
944                int networkId = mConnectionInfo.getNetworkId();
945                if (!mHasConnectedWifiManager) {
946                    mWifiManager.asyncConnect(mContext, getHandler());
947                    mHasConnectedWifiManager = true;
948                }
949                mWifiManager.disableNetwork(networkId, WifiConfiguration.DISABLED_DNS_FAILURE);
950                if (mShowDisabledNotification && mConnectionInfo.isExplicitConnect()) {
951                    displayDisabledNetworkNotification(mConnectionInfo.getSSID());
952                }
953                transitionTo(mNotConnectedState);
954            } else {
955                Slog.i(WWSM_TAG, "Blacklisting current BSSID.  " + wifiInfoToStr(mConnectionInfo)
956                       + "numCheckFailures " + mNumCheckFailures + ", numAPs " + mBssids.size());
957
958                mWifiManager.addToBlacklist(mConnectionInfo.getBSSID());
959                mWifiManager.reassociate();
960                transitionTo(mBlacklistedApState);
961            }
962            return HANDLED;
963        }
964    }
965
966    class WalledGardenState extends State {
967        @Override
968        public void enter() {
969            obtainMessage(MESSAGE_HANDLE_WALLED_GARDEN, mNetEventCounter, 0).sendToTarget();
970        }
971
972        @Override
973        public boolean processMessage(Message msg) {
974            if (msg.what != MESSAGE_HANDLE_WALLED_GARDEN) {
975                return NOT_HANDLED;
976            }
977
978            if (msg.arg1 != mNetEventCounter) {
979                if (VDBG) {
980                    Slog.v(WWSM_TAG, "WalledGardenState::Msg out of sync, ignoring...");
981                }
982                return HANDLED;
983            }
984            popUpBrowser();
985            transitionTo(mOnlineWatchState);
986            return HANDLED;
987        }
988    }
989
990    class BlacklistedApState extends State {
991        @Override
992        public void enter() {
993            mDisableAPNextFailure = true;
994            sendMessageDelayed(obtainMessage(MESSAGE_NETWORK_FOLLOWUP, mNetEventCounter, 0),
995                    mBlacklistFollowupIntervalMs);
996        }
997
998        @Override
999        public boolean processMessage(Message msg) {
1000            if (msg.what != MESSAGE_NETWORK_FOLLOWUP) {
1001                return NOT_HANDLED;
1002            }
1003
1004            if (msg.arg1 != mNetEventCounter) {
1005                if (VDBG) {
1006                    Slog.v(WWSM_TAG, "BlacklistedApState::Msg out of sync, ignoring...");
1007                }
1008                return HANDLED;
1009            }
1010
1011            transitionTo(mDnsCheckingState);
1012            return HANDLED;
1013        }
1014    }
1015
1016
1017    /**
1018     * Convenience function for retrieving a single secure settings value
1019     * as a string with a default value.
1020     *
1021     * @param cr The ContentResolver to access.
1022     * @param name The name of the setting to retrieve.
1023     * @param def Value to return if the setting is not defined.
1024     *
1025     * @return The setting's current value, or 'def' if it is not defined
1026     */
1027    private static String getSettingsStr(ContentResolver cr, String name, String def) {
1028        String v = Settings.Secure.getString(cr, name);
1029        return v != null ? v : def;
1030    }
1031
1032    /**
1033     * Convenience function for retrieving a single secure settings value
1034     * as a boolean.  Note that internally setting values are always
1035     * stored as strings; this function converts the string to a boolean
1036     * for you.  The default value will be returned if the setting is
1037     * not defined or not a valid boolean.
1038     *
1039     * @param cr The ContentResolver to access.
1040     * @param name The name of the setting to retrieve.
1041     * @param def Value to return if the setting is not defined.
1042     *
1043     * @return The setting's current value, or 'def' if it is not defined
1044     * or not a valid boolean.
1045     */
1046    private static boolean getSettingsBoolean(ContentResolver cr, String name, boolean def) {
1047        return Settings.Secure.getInt(cr, name, def ? 1 : 0) == 1;
1048    }
1049
1050    /**
1051     * Convenience function for updating a single settings value as an
1052     * integer. This will either create a new entry in the table if the
1053     * given name does not exist, or modify the value of the existing row
1054     * with that name.  Note that internally setting values are always
1055     * stored as strings, so this function converts the given value to a
1056     * string before storing it.
1057     *
1058     * @param cr The ContentResolver to access.
1059     * @param name The name of the setting to modify.
1060     * @param value The new value for the setting.
1061     * @return true if the value was set, false on database errors
1062     */
1063    private static boolean putSettingsBoolean(ContentResolver cr, String name, boolean value) {
1064        return Settings.Secure.putInt(cr, name, value ? 1 : 0);
1065    }
1066
1067
1068}
1069