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