blob: f2347edebf67cd06ba8604147ba58f1c18f540d4 [file] [log] [blame]
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001/*
2 * Copyright (C) 2008 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.server;
18
19import android.content.BroadcastReceiver;
20import android.content.ContentResolver;
21import android.content.Context;
22import android.content.Intent;
23import android.content.IntentFilter;
24import android.database.ContentObserver;
25import android.net.NetworkInfo;
26import android.net.DhcpInfo;
27import android.net.wifi.ScanResult;
28import android.net.wifi.WifiInfo;
29import android.net.wifi.WifiManager;
30import android.net.wifi.WifiStateTracker;
31import android.os.Handler;
32import android.os.Looper;
33import android.os.Message;
34import android.provider.Settings;
35import android.text.TextUtils;
36import android.util.Config;
37import android.util.Log;
38
39import java.io.IOException;
40import java.net.DatagramPacket;
41import java.net.DatagramSocket;
42import java.net.InetAddress;
43import java.net.SocketException;
44import java.net.SocketTimeoutException;
45import java.net.UnknownHostException;
46import java.util.List;
47import java.util.Random;
48
49/**
50 * {@link WifiWatchdogService} monitors the initial connection to a Wi-Fi
51 * network with multiple access points. After the framework successfully
52 * connects to an access point, the watchdog verifies whether the DNS server is
53 * reachable. If not, the watchdog blacklists the current access point, leading
54 * to a connection on another access point within the same network.
55 * <p>
56 * The watchdog has a few safeguards:
57 * <ul>
58 * <li>Only monitor networks with multiple access points
59 * <li>Only check at most {@link #getMaxApChecks()} different access points
60 * within the network before giving up
61 * <p>
62 * The watchdog checks for connectivity on an access point by ICMP pinging the
63 * DNS. There are settings that allow disabling the watchdog, or tweaking the
64 * acceptable packet loss (and other various parameters).
65 * <p>
66 * The core logic of the watchdog is done on the main watchdog thread. Wi-Fi
67 * callbacks can come in on other threads, so we must queue messages to the main
68 * watchdog thread's handler. Most (if not all) state is only written to from
69 * the main thread.
70 *
71 * {@hide}
72 */
73public class WifiWatchdogService {
74 private static final String TAG = "WifiWatchdogService";
75 private static final boolean V = false || Config.LOGV;
76 private static final boolean D = true || Config.LOGD;
77
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080078 private Context mContext;
79 private ContentResolver mContentResolver;
80 private WifiStateTracker mWifiStateTracker;
81 private WifiManager mWifiManager;
82
83 /**
84 * The main watchdog thread.
85 */
86 private WifiWatchdogThread mThread;
87 /**
88 * The handler for the main watchdog thread.
89 */
90 private WifiWatchdogHandler mHandler;
91
92 /**
93 * The current watchdog state. Only written from the main thread!
94 */
95 private WatchdogState mState = WatchdogState.IDLE;
96 /**
97 * The SSID of the network that the watchdog is currently monitoring. Only
98 * touched in the main thread!
99 */
100 private String mSsid;
101 /**
102 * The number of access points in the current network ({@link #mSsid}) that
103 * have been checked. Only touched in the main thread!
104 */
105 private int mNumApsChecked;
106 /** Whether the current AP check should be canceled. */
107 private boolean mShouldCancel;
108
109 WifiWatchdogService(Context context, WifiStateTracker wifiStateTracker) {
110 mContext = context;
111 mContentResolver = context.getContentResolver();
112 mWifiStateTracker = wifiStateTracker;
113 mWifiManager = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
114
115 createThread();
116
117 // The content observer to listen needs a handler, which createThread creates
118 registerForSettingsChanges();
119 if (isWatchdogEnabled()) {
120 registerForWifiBroadcasts();
121 }
122
123 if (V) {
124 myLogV("WifiWatchdogService: Created");
125 }
126 }
127
128 /**
129 * Observes the watchdog on/off setting, and takes action when changed.
130 */
131 private void registerForSettingsChanges() {
132 ContentResolver contentResolver = mContext.getContentResolver();
133 contentResolver.registerContentObserver(
134 Settings.Secure.getUriFor(Settings.Secure.WIFI_WATCHDOG_ON), false,
135 new ContentObserver(mHandler) {
136 @Override
137 public void onChange(boolean selfChange) {
138 if (isWatchdogEnabled()) {
139 registerForWifiBroadcasts();
140 } else {
141 unregisterForWifiBroadcasts();
142 if (mHandler != null) {
143 mHandler.disableWatchdog();
144 }
145 }
146 }
147 });
148 }
149
150 /**
151 * @see android.provider.Settings.Secure#WIFI_WATCHDOG_ON
152 */
153 private boolean isWatchdogEnabled() {
154 return Settings.Secure.getInt(mContentResolver, Settings.Secure.WIFI_WATCHDOG_ON, 1) == 1;
155 }
156
157 /**
158 * @see android.provider.Settings.Secure#WIFI_WATCHDOG_AP_COUNT
159 */
160 private int getApCount() {
161 return Settings.Secure.getInt(mContentResolver,
162 Settings.Secure.WIFI_WATCHDOG_AP_COUNT, 2);
163 }
164
165 /**
166 * @see android.provider.Settings.Secure#WIFI_WATCHDOG_INITIAL_IGNORED_PING_COUNT
167 */
168 private int getInitialIgnoredPingCount() {
169 return Settings.Secure.getInt(mContentResolver,
170 Settings.Secure.WIFI_WATCHDOG_INITIAL_IGNORED_PING_COUNT , 2);
171 }
172
173 /**
174 * @see android.provider.Settings.Secure#WIFI_WATCHDOG_PING_COUNT
175 */
176 private int getPingCount() {
177 return Settings.Secure.getInt(mContentResolver,
178 Settings.Secure.WIFI_WATCHDOG_PING_COUNT, 4);
179 }
180
181 /**
182 * @see android.provider.Settings.Secure#WIFI_WATCHDOG_PING_TIMEOUT_MS
183 */
184 private int getPingTimeoutMs() {
185 return Settings.Secure.getInt(mContentResolver,
186 Settings.Secure.WIFI_WATCHDOG_PING_TIMEOUT_MS, 500);
187 }
188
189 /**
190 * @see android.provider.Settings.Secure#WIFI_WATCHDOG_PING_DELAY_MS
191 */
192 private int getPingDelayMs() {
193 return Settings.Secure.getInt(mContentResolver,
194 Settings.Secure.WIFI_WATCHDOG_PING_DELAY_MS, 250);
195 }
196
197 /**
198 * @see android.provider.Settings.Secure#WIFI_WATCHDOG_ACCEPTABLE_PACKET_LOSS_PERCENTAGE
199 */
200 private int getAcceptablePacketLossPercentage() {
201 return Settings.Secure.getInt(mContentResolver,
202 Settings.Secure.WIFI_WATCHDOG_ACCEPTABLE_PACKET_LOSS_PERCENTAGE, 25);
203 }
204
205 /**
206 * @see android.provider.Settings.Secure#WIFI_WATCHDOG_MAX_AP_CHECKS
207 */
208 private int getMaxApChecks() {
209 return Settings.Secure.getInt(mContentResolver,
210 Settings.Secure.WIFI_WATCHDOG_MAX_AP_CHECKS, 7);
211 }
212
213 /**
214 * @see android.provider.Settings.Secure#WIFI_WATCHDOG_BACKGROUND_CHECK_ENABLED
215 */
216 private boolean isBackgroundCheckEnabled() {
217 return Settings.Secure.getInt(mContentResolver,
218 Settings.Secure.WIFI_WATCHDOG_BACKGROUND_CHECK_ENABLED, 1) == 1;
219 }
220
221 /**
222 * @see android.provider.Settings.Secure#WIFI_WATCHDOG_BACKGROUND_CHECK_DELAY_MS
223 */
224 private int getBackgroundCheckDelayMs() {
225 return Settings.Secure.getInt(mContentResolver,
226 Settings.Secure.WIFI_WATCHDOG_BACKGROUND_CHECK_DELAY_MS, 60000);
227 }
228
229 /**
230 * @see android.provider.Settings.Secure#WIFI_WATCHDOG_BACKGROUND_CHECK_TIMEOUT_MS
231 */
232 private int getBackgroundCheckTimeoutMs() {
233 return Settings.Secure.getInt(mContentResolver,
234 Settings.Secure.WIFI_WATCHDOG_BACKGROUND_CHECK_TIMEOUT_MS, 1000);
235 }
236
237 /**
238 * @see android.provider.Settings.Secure#WIFI_WATCHDOG_WATCH_LIST
239 * @return the comma-separated list of SSIDs
240 */
241 private String getWatchList() {
242 return Settings.Secure.getString(mContentResolver,
243 Settings.Secure.WIFI_WATCHDOG_WATCH_LIST);
244 }
245
246 /**
247 * Registers to receive the necessary Wi-Fi broadcasts.
248 */
249 private void registerForWifiBroadcasts() {
250 IntentFilter intentFilter = new IntentFilter();
251 intentFilter.addAction(WifiManager.NETWORK_STATE_CHANGED_ACTION);
252 intentFilter.addAction(WifiManager.SUPPLICANT_CONNECTION_CHANGE_ACTION);
253 intentFilter.addAction(WifiManager.WIFI_STATE_CHANGED_ACTION);
254 mContext.registerReceiver(mReceiver, intentFilter);
255 }
256
257 /**
258 * Unregisters from receiving the Wi-Fi broadcasts.
259 */
260 private void unregisterForWifiBroadcasts() {
261 mContext.unregisterReceiver(mReceiver);
262 }
263
264 /**
265 * Creates the main watchdog thread, including waiting for the handler to be
266 * created.
267 */
268 private void createThread() {
269 mThread = new WifiWatchdogThread();
270 mThread.start();
271 waitForHandlerCreation();
272 }
273
274 /**
275 * Waits for the main watchdog thread to create the handler.
276 */
277 private void waitForHandlerCreation() {
278 synchronized(this) {
279 while (mHandler == null) {
280 try {
281 // Wait for the handler to be set by the other thread
282 wait();
283 } catch (InterruptedException e) {
284 Log.e(TAG, "Interrupted while waiting on handler.");
285 }
286 }
287 }
288 }
289
290 // Utility methods
291
292 /**
293 * Logs with the current thread.
294 */
295 private static void myLogV(String message) {
296 Log.v(TAG, "(" + Thread.currentThread().getName() + ") " + message);
297 }
298
299 private static void myLogD(String message) {
300 Log.d(TAG, "(" + Thread.currentThread().getName() + ") " + message);
301 }
302
303 /**
304 * Gets the DNS of the current AP.
305 *
306 * @return The DNS of the current AP.
307 */
308 private int getDns() {
309 DhcpInfo addressInfo = mWifiManager.getDhcpInfo();
310 if (addressInfo != null) {
311 return addressInfo.dns1;
312 } else {
313 return -1;
314 }
315 }
316
317 /**
318 * Checks whether the DNS can be reached using multiple attempts according
319 * to the current setting values.
320 *
321 * @return Whether the DNS is reachable
322 */
323 private boolean checkDnsConnectivity() {
324 int dns = getDns();
325 if (dns == -1) {
326 if (V) {
327 myLogV("checkDnsConnectivity: Invalid DNS, returning false");
328 }
329 return false;
330 }
331
332 if (V) {
333 myLogV("checkDnsConnectivity: Checking 0x" +
334 Integer.toHexString(Integer.reverseBytes(dns)) + " for connectivity");
335 }
336
337 int numInitialIgnoredPings = getInitialIgnoredPingCount();
338 int numPings = getPingCount();
339 int pingDelay = getPingDelayMs();
340 int acceptableLoss = getAcceptablePacketLossPercentage();
341
342 /** See {@link Secure#WIFI_WATCHDOG_INITIAL_IGNORED_PING_COUNT} */
343 int ignoredPingCounter = 0;
344 int pingCounter = 0;
345 int successCounter = 0;
346
347 // No connectivity check needed
348 if (numPings == 0) {
349 return true;
350 }
351
352 // Do the initial pings that we ignore
353 for (; ignoredPingCounter < numInitialIgnoredPings; ignoredPingCounter++) {
354 if (shouldCancel()) return false;
355
356 boolean dnsAlive = DnsPinger.isDnsReachable(dns, getPingTimeoutMs());
357 if (dnsAlive) {
358 /*
359 * Successful "ignored" pings are *not* ignored (they count in the total number
360 * of pings), but failures are really ignored.
361 */
362 pingCounter++;
363 successCounter++;
364 }
365
366 if (V) {
367 Log.v(TAG, (dnsAlive ? " +" : " Ignored: -"));
368 }
369
370 if (shouldCancel()) return false;
371
372 try {
373 Thread.sleep(pingDelay);
374 } catch (InterruptedException e) {
375 Log.w(TAG, "Interrupted while pausing between pings", e);
376 }
377 }
378
379 // Do the pings that we use to measure packet loss
380 for (; pingCounter < numPings; pingCounter++) {
381 if (shouldCancel()) return false;
382
383 if (DnsPinger.isDnsReachable(dns, getPingTimeoutMs())) {
384 successCounter++;
385 if (V) {
386 Log.v(TAG, " +");
387 }
388 } else {
389 if (V) {
390 Log.v(TAG, " -");
391 }
392 }
393
394 if (shouldCancel()) return false;
395
396 try {
397 Thread.sleep(pingDelay);
398 } catch (InterruptedException e) {
399 Log.w(TAG, "Interrupted while pausing between pings", e);
400 }
401 }
402
403 int packetLossPercentage = 100 * (numPings - successCounter) / numPings;
404 if (D) {
405 Log.d(TAG, packetLossPercentage
406 + "% packet loss (acceptable is " + acceptableLoss + "%)");
407 }
408
409 return !shouldCancel() && (packetLossPercentage <= acceptableLoss);
410 }
411
412 private boolean backgroundCheckDnsConnectivity() {
413 int dns = getDns();
414 if (false && V) {
415 myLogV("backgroundCheckDnsConnectivity: Background checking " + dns +
416 " for connectivity");
417 }
418
419 if (dns == -1) {
420 if (V) {
421 myLogV("backgroundCheckDnsConnectivity: DNS is empty, returning false");
422 }
423 return false;
424 }
425
426 return DnsPinger.isDnsReachable(dns, getBackgroundCheckTimeoutMs());
427 }
428
429 /**
430 * Signals the current action to cancel.
431 */
432 private void cancelCurrentAction() {
433 mShouldCancel = true;
434 }
435
436 /**
437 * Helper to check whether to cancel.
438 *
439 * @return Whether to cancel processing the action.
440 */
441 private boolean shouldCancel() {
442 if (V && mShouldCancel) {
443 myLogV("shouldCancel: Cancelling");
444 }
445
446 return mShouldCancel;
447 }
448
449 // Wi-Fi initiated callbacks (could be executed in another thread)
450
451 /**
452 * Called when connected to an AP (this can be the next AP in line, or
453 * it can be a completely different network).
454 *
455 * @param ssid The SSID of the access point.
456 * @param bssid The BSSID of the access point.
457 */
458 private void onConnected(String ssid, String bssid) {
459 if (V) {
460 myLogV("onConnected: SSID: " + ssid + ", BSSID: " + bssid);
461 }
462
463 /*
464 * The current action being processed by the main watchdog thread is now
465 * stale, so cancel it.
466 */
467 cancelCurrentAction();
468
469 if ((mSsid == null) || !mSsid.equals(ssid)) {
470 /*
471 * This is a different network than what the main watchdog thread is
472 * processing, dispatch the network change message on the main thread.
473 */
474 mHandler.dispatchNetworkChanged(ssid);
475 }
476
477 if (requiresWatchdog(ssid, bssid)) {
478 if (D) {
479 myLogD(ssid + " (" + bssid + ") requires the watchdog");
480 }
481
482 // This access point requires a watchdog, so queue the check on the main thread
483 mHandler.checkAp(new AccessPoint(ssid, bssid));
484
485 } else {
486 if (D) {
487 myLogD(ssid + " (" + bssid + ") does not require the watchdog");
488 }
489
490 // This access point does not require a watchdog, so queue idle on the main thread
491 mHandler.idle();
492 }
493 }
494
495 /**
496 * Called when Wi-Fi is enabled.
497 */
498 private void onEnabled() {
499 cancelCurrentAction();
500 // Queue a hard-reset of the state on the main thread
501 mHandler.reset();
502 }
503
504 /**
505 * Called when disconnected (or some other event similar to being disconnected).
506 */
507 private void onDisconnected() {
508 if (V) {
509 myLogV("onDisconnected");
510 }
511
512 /*
513 * Disconnected from an access point, the action being processed by the
514 * watchdog thread is now stale, so cancel it.
515 */
516 cancelCurrentAction();
517 // Dispatch the disconnected to the main watchdog thread
518 mHandler.dispatchDisconnected();
519 // Queue the action to go idle
520 mHandler.idle();
521 }
522
523 /**
524 * Checks whether an access point requires watchdog monitoring.
525 *
526 * @param ssid The SSID of the access point.
527 * @param bssid The BSSID of the access point.
528 * @return Whether the access point/network should be monitored by the
529 * watchdog.
530 */
531 private boolean requiresWatchdog(String ssid, String bssid) {
532 if (V) {
533 myLogV("requiresWatchdog: SSID: " + ssid + ", BSSID: " + bssid);
534 }
535
536 WifiInfo info = null;
537 if (ssid == null) {
538 /*
539 * This is called from a Wi-Fi callback, so assume the WifiInfo does
540 * not have stale data.
541 */
542 info = mWifiManager.getConnectionInfo();
543 ssid = info.getSSID();
544 if (ssid == null) {
545 // It's still null, give up
546 if (V) {
547 Log.v(TAG, " Invalid SSID, returning false");
548 }
549 return false;
550 }
551 }
552
553 if (TextUtils.isEmpty(bssid)) {
554 // Similar as above
555 if (info == null) {
556 info = mWifiManager.getConnectionInfo();
557 }
558 bssid = info.getBSSID();
559 if (TextUtils.isEmpty(bssid)) {
560 // It's still null, give up
561 if (V) {
562 Log.v(TAG, " Invalid BSSID, returning false");
563 }
564 return false;
565 }
566 }
567
568 if (!isOnWatchList(ssid)) {
569 if (V) {
570 Log.v(TAG, " SSID not on watch list, returning false");
571 }
572 return false;
573 }
574
575 // The watchdog only monitors networks with multiple APs
576 if (!hasRequiredNumberOfAps(ssid)) {
577 return false;
578 }
579
580 return true;
581 }
582
583 private boolean isOnWatchList(String ssid) {
584 String watchList;
585
586 if (ssid == null || (watchList = getWatchList()) == null) {
587 return false;
588 }
589
590 String[] list = watchList.split(" *, *");
591
592 for (String name : list) {
593 if (ssid.equals(name)) {
594 return true;
595 }
596 }
597
598 return false;
599 }
600
601 /**
602 * Checks if the current scan results have multiple access points with an SSID.
603 *
604 * @param ssid The SSID to check.
605 * @return Whether the SSID has multiple access points.
606 */
607 private boolean hasRequiredNumberOfAps(String ssid) {
608 List<ScanResult> results = mWifiManager.getScanResults();
609 if (results == null) {
610 if (V) {
611 myLogV("hasRequiredNumberOfAps: Got null scan results, returning false");
612 }
613 return false;
614 }
615
616 int numApsRequired = getApCount();
617 int numApsFound = 0;
618 int resultsSize = results.size();
619 for (int i = 0; i < resultsSize; i++) {
620 ScanResult result = results.get(i);
621 if (result == null) continue;
622 if (result.SSID == null) continue;
623
624 if (result.SSID.equals(ssid)) {
625 numApsFound++;
626
627 if (numApsFound >= numApsRequired) {
628 if (V) {
629 myLogV("hasRequiredNumberOfAps: SSID: " + ssid + ", returning true");
630 }
631 return true;
632 }
633 }
634 }
635
636 if (V) {
637 myLogV("hasRequiredNumberOfAps: SSID: " + ssid + ", returning false");
638 }
639 return false;
640 }
641
642 // Watchdog logic (assume all of these methods will be in our main thread)
643
644 /**
645 * Handles a Wi-Fi network change (for example, from networkA to networkB).
646 */
647 private void handleNetworkChanged(String ssid) {
648 // Set the SSID being monitored to the new SSID
649 mSsid = ssid;
650 // Set various state to that when being idle
651 setIdleState(true);
652 }
653
654 /**
655 * Handles checking whether an AP is a "good" AP. If not, it will be blacklisted.
656 *
657 * @param ap The access point to check.
658 */
659 private void handleCheckAp(AccessPoint ap) {
660 // Reset the cancel state since this is the entry point of this action
661 mShouldCancel = false;
662
663 if (V) {
664 myLogV("handleCheckAp: AccessPoint: " + ap);
665 }
666
667 // Make sure we are not sleeping
668 if (mState == WatchdogState.SLEEP) {
669 if (V) {
670 Log.v(TAG, " Sleeping (in " + mSsid + "), so returning");
671 }
672 return;
673 }
674
675 mState = WatchdogState.CHECKING_AP;
676
677 /*
678 * Checks to make sure we haven't exceeded the max number of checks
679 * we're allowed per network
680 */
681 mNumApsChecked++;
682 if (mNumApsChecked > getMaxApChecks()) {
683 if (V) {
684 Log.v(TAG, " Passed the max attempts (" + getMaxApChecks()
685 + "), going to sleep for " + mSsid);
686 }
687 mHandler.sleep(mSsid);
688 return;
689 }
690
691 // Do the check
692 boolean isApAlive = checkDnsConnectivity();
693
694 if (V) {
695 Log.v(TAG, " Is it alive: " + isApAlive);
696 }
697
698 // Take action based on results
699 if (isApAlive) {
700 handleApAlive(ap);
701 } else {
702 handleApUnresponsive(ap);
703 }
704 }
705
706 /**
707 * Handles the case when an access point is alive.
708 *
709 * @param ap The access point.
710 */
711 private void handleApAlive(AccessPoint ap) {
712 // Check whether we are stale and should cancel
713 if (shouldCancel()) return;
714 // We're satisfied with this AP, so go idle
715 setIdleState(false);
716
717 if (D) {
718 myLogD("AP is alive: " + ap.toString());
719 }
720
721 // Queue the next action to be a background check
722 mHandler.backgroundCheckAp(ap);
723 }
724
725 /**
726 * Handles an unresponsive AP by blacklisting it.
727 *
728 * @param ap The access point.
729 */
730 private void handleApUnresponsive(AccessPoint ap) {
731 // Check whether we are stale and should cancel
732 if (shouldCancel()) return;
733 // This AP is "bad", switch to another
734 mState = WatchdogState.SWITCHING_AP;
735
736 if (D) {
737 myLogD("AP is dead: " + ap.toString());
738 }
739
740 // Black list this "bad" AP, this will cause an attempt to connect to another
741 blacklistAp(ap.bssid);
Irfan Sheriff0049a1b2010-01-14 12:37:49 -0800742 // Initiate an association to an alternate AP
743 mWifiStateTracker.reassociate();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800744 }
745
746 private void blacklistAp(String bssid) {
747 if (TextUtils.isEmpty(bssid)) {
748 return;
749 }
750
751 // Before taking action, make sure we should not cancel our processing
752 if (shouldCancel()) return;
753
754 if (!mWifiStateTracker.addToBlacklist(bssid)) {
755 // There's a known bug where this method returns failure on success
756 //Log.e(TAG, "Blacklisting " + bssid + " failed");
757 }
758
759 if (D) {
760 myLogD("Blacklisting " + bssid);
761 }
762 }
763
764 /**
765 * Handles a single background check. If it fails, it should trigger a
766 * normal check. If it succeeds, it should queue another background check.
767 *
768 * @param ap The access point to do a background check for. If this is no
769 * longer the current AP, it is okay to return without any
770 * processing.
771 */
772 private void handleBackgroundCheckAp(AccessPoint ap) {
773 // Reset the cancel state since this is the entry point of this action
774 mShouldCancel = false;
775
776 if (false && V) {
777 myLogV("handleBackgroundCheckAp: AccessPoint: " + ap);
778 }
779
780 // Make sure we are not sleeping
781 if (mState == WatchdogState.SLEEP) {
782 if (V) {
783 Log.v(TAG, " handleBackgroundCheckAp: Sleeping (in " + mSsid + "), so returning");
784 }
785 return;
786 }
787
788 // Make sure the AP we're supposed to be background checking is still the active one
789 WifiInfo info = mWifiManager.getConnectionInfo();
790 if (info.getSSID() == null || !info.getSSID().equals(ap.ssid)) {
791 if (V) {
792 myLogV("handleBackgroundCheckAp: We are no longer connected to "
793 + ap + ", and instead are on " + info);
794 }
795 return;
796 }
797
798 if (info.getBSSID() == null || !info.getBSSID().equals(ap.bssid)) {
799 if (V) {
800 myLogV("handleBackgroundCheckAp: We are no longer connected to "
801 + ap + ", and instead are on " + info);
802 }
803 return;
804 }
805
806 // Do the check
807 boolean isApAlive = backgroundCheckDnsConnectivity();
808
809 if (V && !isApAlive) {
810 Log.v(TAG, " handleBackgroundCheckAp: Is it alive: " + isApAlive);
811 }
812
813 if (shouldCancel()) {
814 return;
815 }
816
817 // Take action based on results
818 if (isApAlive) {
819 // Queue another background check
820 mHandler.backgroundCheckAp(ap);
821
822 } else {
823 if (D) {
824 myLogD("Background check failed for " + ap.toString());
825 }
826
827 // Queue a normal check, so it can take proper action
828 mHandler.checkAp(ap);
829 }
830 }
831
832 /**
833 * Handles going to sleep for this network. Going to sleep means we will not
834 * monitor this network anymore.
835 *
836 * @param ssid The network that will not be monitored anymore.
837 */
838 private void handleSleep(String ssid) {
839 // Make sure the network we're trying to sleep in is still the current network
840 if (ssid != null && ssid.equals(mSsid)) {
841 mState = WatchdogState.SLEEP;
842
843 if (D) {
844 myLogD("Going to sleep for " + ssid);
845 }
846
847 /*
848 * Before deciding to go to sleep, we may have checked a few APs
849 * (and blacklisted them). Clear the blacklist so the AP with best
850 * signal is chosen.
851 */
852 if (!mWifiStateTracker.clearBlacklist()) {
853 // There's a known bug where this method returns failure on success
854 //Log.e(TAG, "Clearing blacklist failed");
855 }
856
857 if (V) {
858 myLogV("handleSleep: Set state to SLEEP and cleared blacklist");
859 }
860 }
861 }
862
863 /**
864 * Handles an access point disconnection.
865 */
866 private void handleDisconnected() {
867 /*
868 * We purposefully do not change mSsid to null. This is to handle
869 * disconnected followed by connected better (even if there is some
870 * duration in between). For example, if the watchdog went to sleep in a
871 * network, and then the phone goes to sleep, when the phone wakes up we
872 * still want to be in the sleeping state. When the phone went to sleep,
873 * we would have gotten a disconnected event which would then set mSsid
874 * = null. This is bad, since the following connect would cause us to do
875 * the "network is good?" check all over again. */
876
877 /*
878 * Set the state as if we were idle (don't come out of sleep, only
879 * hard reset and network changed should do that.
880 */
881 setIdleState(false);
882 }
883
884 /**
885 * Handles going idle. Idle means we are satisfied with the current state of
886 * things, but if a new connection occurs we'll re-evaluate.
887 */
888 private void handleIdle() {
889 // Reset the cancel state since this is the entry point for this action
890 mShouldCancel = false;
891
892 if (V) {
893 myLogV("handleSwitchToIdle");
894 }
895
896 // If we're sleeping, don't do anything
897 if (mState == WatchdogState.SLEEP) {
898 Log.v(TAG, " Sleeping (in " + mSsid + "), so returning");
899 return;
900 }
901
902 // Set the idle state
903 setIdleState(false);
904
905 if (V) {
906 Log.v(TAG, " Set state to IDLE");
907 }
908 }
909
910 /**
911 * Sets the state as if we are going idle.
912 */
913 private void setIdleState(boolean forceIdleState) {
914 // Setting idle state does not kick us out of sleep unless the forceIdleState is set
915 if (forceIdleState || (mState != WatchdogState.SLEEP)) {
916 mState = WatchdogState.IDLE;
917 }
918 mNumApsChecked = 0;
919 }
920
921 /**
922 * Handles a hard reset. A hard reset is rarely used, but when used it
923 * should revert anything done by the watchdog monitoring.
924 */
925 private void handleReset() {
926 mWifiStateTracker.clearBlacklist();
927 setIdleState(true);
928 }
929
930 // Inner classes
931
932 /**
933 * Possible states for the watchdog to be in.
934 */
935 private static enum WatchdogState {
936 /** The watchdog is currently idle, but it is still responsive to future AP checks in this network. */
937 IDLE,
938 /** The watchdog is sleeping, so it will not try any AP checks for the network. */
939 SLEEP,
940 /** The watchdog is currently checking an AP for connectivity. */
941 CHECKING_AP,
942 /** The watchdog is switching to another AP in the network. */
943 SWITCHING_AP
944 }
945
946 /**
947 * The main thread for the watchdog monitoring. This will be turned into a
948 * {@link Looper} thread.
949 */
950 private class WifiWatchdogThread extends Thread {
951 WifiWatchdogThread() {
952 super("WifiWatchdogThread");
953 }
954
955 @Override
956 public void run() {
957 // Set this thread up so the handler will work on it
958 Looper.prepare();
959
960 synchronized(WifiWatchdogService.this) {
961 mHandler = new WifiWatchdogHandler();
962
963 // Notify that the handler has been created
964 WifiWatchdogService.this.notify();
965 }
966
967 // Listen for messages to the handler
968 Looper.loop();
969 }
970 }
971
972 /**
973 * The main thread's handler. There are 'actions', and just general
974 * 'messages'. There should only ever be one 'action' in the queue (aside
975 * from the one being processed, if any). There may be multiple messages in
976 * the queue. So, actions are replaced by more recent actions, where as
977 * messages will be executed for sure. Messages end up being used to just
978 * change some state, and not really take any action.
979 * <p>
980 * There is little logic inside this class, instead methods of the form
981 * "handle___" are called in the main {@link WifiWatchdogService}.
982 */
983 private class WifiWatchdogHandler extends Handler {
984 /** Check whether the AP is "good". The object will be an {@link AccessPoint}. */
985 static final int ACTION_CHECK_AP = 1;
986 /** Go into the idle state. */
987 static final int ACTION_IDLE = 2;
988 /**
989 * Performs a periodic background check whether the AP is still "good".
990 * The object will be an {@link AccessPoint}.
991 */
992 static final int ACTION_BACKGROUND_CHECK_AP = 3;
993
994 /**
995 * Go to sleep for the current network. We are conservative with making
996 * this a message rather than action. We want to make sure our main
997 * thread sees this message, but if it were an action it could be
998 * removed from the queue and replaced by another action. The main
999 * thread will ensure when it sees the message that the state is still
1000 * valid for going to sleep.
1001 * <p>
1002 * For an explanation of sleep, see {@link android.provider.Settings.Secure#WIFI_WATCHDOG_MAX_AP_CHECKS}.
1003 */
1004 static final int MESSAGE_SLEEP = 101;
1005 /** Disables the watchdog. */
1006 static final int MESSAGE_DISABLE_WATCHDOG = 102;
1007 /** The network has changed. */
1008 static final int MESSAGE_NETWORK_CHANGED = 103;
1009 /** The current access point has disconnected. */
1010 static final int MESSAGE_DISCONNECTED = 104;
1011 /** Performs a hard-reset on the watchdog state. */
1012 static final int MESSAGE_RESET = 105;
1013
1014 void checkAp(AccessPoint ap) {
1015 removeAllActions();
1016 sendMessage(obtainMessage(ACTION_CHECK_AP, ap));
1017 }
1018
1019 void backgroundCheckAp(AccessPoint ap) {
1020 if (!isBackgroundCheckEnabled()) return;
1021
1022 removeAllActions();
1023 sendMessageDelayed(obtainMessage(ACTION_BACKGROUND_CHECK_AP, ap),
1024 getBackgroundCheckDelayMs());
1025 }
1026
1027 void idle() {
1028 removeAllActions();
1029 sendMessage(obtainMessage(ACTION_IDLE));
1030 }
1031
1032 void sleep(String ssid) {
1033 removeAllActions();
1034 sendMessage(obtainMessage(MESSAGE_SLEEP, ssid));
1035 }
1036
1037 void disableWatchdog() {
1038 removeAllActions();
1039 sendMessage(obtainMessage(MESSAGE_DISABLE_WATCHDOG));
1040 }
1041
1042 void dispatchNetworkChanged(String ssid) {
1043 removeAllActions();
1044 sendMessage(obtainMessage(MESSAGE_NETWORK_CHANGED, ssid));
1045 }
1046
1047 void dispatchDisconnected() {
1048 removeAllActions();
1049 sendMessage(obtainMessage(MESSAGE_DISCONNECTED));
1050 }
1051
1052 void reset() {
1053 removeAllActions();
1054 sendMessage(obtainMessage(MESSAGE_RESET));
1055 }
1056
1057 private void removeAllActions() {
1058 removeMessages(ACTION_CHECK_AP);
1059 removeMessages(ACTION_IDLE);
1060 removeMessages(ACTION_BACKGROUND_CHECK_AP);
1061 }
1062
1063 @Override
1064 public void handleMessage(Message msg) {
1065 switch (msg.what) {
1066 case MESSAGE_NETWORK_CHANGED:
1067 handleNetworkChanged((String) msg.obj);
1068 break;
1069 case ACTION_CHECK_AP:
1070 handleCheckAp((AccessPoint) msg.obj);
1071 break;
1072 case ACTION_BACKGROUND_CHECK_AP:
1073 handleBackgroundCheckAp((AccessPoint) msg.obj);
1074 break;
1075 case MESSAGE_SLEEP:
1076 handleSleep((String) msg.obj);
1077 break;
1078 case ACTION_IDLE:
1079 handleIdle();
1080 break;
1081 case MESSAGE_DISABLE_WATCHDOG:
1082 handleIdle();
1083 break;
1084 case MESSAGE_DISCONNECTED:
1085 handleDisconnected();
1086 break;
1087 case MESSAGE_RESET:
1088 handleReset();
1089 break;
1090 }
1091 }
1092 }
1093
1094 /**
1095 * Receives Wi-Fi broadcasts.
1096 * <p>
1097 * There is little logic in this class, instead methods of the form "on___"
1098 * are called in the {@link WifiWatchdogService}.
1099 */
1100 private BroadcastReceiver mReceiver = new BroadcastReceiver() {
1101
1102 @Override
1103 public void onReceive(Context context, Intent intent) {
1104 final String action = intent.getAction();
1105 if (action.equals(WifiManager.NETWORK_STATE_CHANGED_ACTION)) {
1106 handleNetworkStateChanged(
1107 (NetworkInfo) intent.getParcelableExtra(WifiManager.EXTRA_NETWORK_INFO));
1108 } else if (action.equals(WifiManager.SUPPLICANT_CONNECTION_CHANGE_ACTION)) {
1109 handleSupplicantConnectionChanged(
1110 intent.getBooleanExtra(WifiManager.EXTRA_SUPPLICANT_CONNECTED, false));
1111 } else if (action.equals(WifiManager.WIFI_STATE_CHANGED_ACTION)) {
1112 handleWifiStateChanged(intent.getIntExtra(WifiManager.EXTRA_WIFI_STATE,
1113 WifiManager.WIFI_STATE_UNKNOWN));
1114 }
1115 }
1116
1117 private void handleNetworkStateChanged(NetworkInfo info) {
1118 if (V) {
1119 myLogV("Receiver.handleNetworkStateChanged: NetworkInfo: "
1120 + info);
1121 }
1122
1123 switch (info.getState()) {
1124 case CONNECTED:
1125 WifiInfo wifiInfo = mWifiManager.getConnectionInfo();
1126 if (wifiInfo.getSSID() == null || wifiInfo.getBSSID() == null) {
1127 if (V) {
1128 myLogV("handleNetworkStateChanged: Got connected event but SSID or BSSID are null. SSID: "
1129 + wifiInfo.getSSID()
1130 + ", BSSID: "
1131 + wifiInfo.getBSSID() + ", ignoring event");
1132 }
1133 return;
1134 }
1135 onConnected(wifiInfo.getSSID(), wifiInfo.getBSSID());
1136 break;
1137
1138 case DISCONNECTED:
1139 onDisconnected();
1140 break;
1141 }
1142 }
1143
1144 private void handleSupplicantConnectionChanged(boolean connected) {
1145 if (!connected) {
1146 onDisconnected();
1147 }
1148 }
1149
1150 private void handleWifiStateChanged(int wifiState) {
1151 if (wifiState == WifiManager.WIFI_STATE_DISABLED) {
1152 onDisconnected();
1153 } else if (wifiState == WifiManager.WIFI_STATE_ENABLED) {
1154 onEnabled();
1155 }
1156 }
1157 };
1158
1159 /**
1160 * Describes an access point by its SSID and BSSID.
1161 */
1162 private static class AccessPoint {
1163 String ssid;
1164 String bssid;
1165
1166 AccessPoint(String ssid, String bssid) {
1167 this.ssid = ssid;
1168 this.bssid = bssid;
1169 }
1170
1171 private boolean hasNull() {
1172 return ssid == null || bssid == null;
1173 }
1174
1175 @Override
1176 public boolean equals(Object o) {
1177 if (!(o instanceof AccessPoint)) return false;
1178 AccessPoint otherAp = (AccessPoint) o;
1179 boolean iHaveNull = hasNull();
1180 // Either we both have a null, or our SSIDs and BSSIDs are equal
1181 return (iHaveNull && otherAp.hasNull()) ||
1182 (otherAp.bssid != null && ssid.equals(otherAp.ssid)
1183 && bssid.equals(otherAp.bssid));
1184 }
1185
1186 @Override
1187 public int hashCode() {
1188 if (ssid == null || bssid == null) return 0;
1189 return ssid.hashCode() + bssid.hashCode();
1190 }
1191
1192 @Override
1193 public String toString() {
1194 return ssid + " (" + bssid + ")";
1195 }
1196 }
1197
1198 /**
1199 * Performs a simple DNS "ping" by sending a "server status" query packet to
1200 * the DNS server. As long as the server replies, we consider it a success.
1201 * <p>
1202 * We do not use a simple hostname lookup because that could be cached and
1203 * the API may not differentiate between a time out and a failure lookup
1204 * (which we really care about).
1205 */
1206 private static class DnsPinger {
1207
1208 /** Number of bytes for the query */
1209 private static final int DNS_QUERY_BASE_SIZE = 33;
1210
1211 /** The DNS port */
1212 private static final int DNS_PORT = 53;
1213
1214 /** Used to generate IDs */
1215 private static Random sRandom = new Random();
1216
1217 static boolean isDnsReachable(int dns, int timeout) {
1218 try {
1219 DatagramSocket socket = new DatagramSocket();
1220
1221 // Set some socket properties
1222 socket.setSoTimeout(timeout);
1223
1224 byte[] buf = new byte[DNS_QUERY_BASE_SIZE];
1225 fillQuery(buf);
1226
1227 // Send the DNS query
1228 byte parts[] = new byte[4];
1229 parts[0] = (byte)(dns & 0xff);
1230 parts[1] = (byte)((dns >> 8) & 0xff);
1231 parts[2] = (byte)((dns >> 16) & 0xff);
1232 parts[3] = (byte)((dns >> 24) & 0xff);
1233
1234 InetAddress dnsAddress = InetAddress.getByAddress(parts);
1235 DatagramPacket packet = new DatagramPacket(buf,
1236 buf.length, dnsAddress, DNS_PORT);
1237 socket.send(packet);
1238
1239 // Wait for reply (blocks for the above timeout)
1240 DatagramPacket replyPacket = new DatagramPacket(buf, buf.length);
1241 socket.receive(replyPacket);
1242
1243 // If a timeout occurred, an exception would have been thrown. We got a reply!
1244 return true;
1245
1246 } catch (SocketException e) {
1247 if (V) {
1248 Log.v(TAG, "DnsPinger.isReachable received SocketException", e);
1249 }
1250 return false;
1251
1252 } catch (UnknownHostException e) {
1253 if (V) {
1254 Log.v(TAG, "DnsPinger.isReachable is unable to resolve the DNS host", e);
1255 }
1256 return false;
1257
1258 } catch (SocketTimeoutException e) {
1259 return false;
1260
1261 } catch (IOException e) {
1262 if (V) {
1263 Log.v(TAG, "DnsPinger.isReachable got an IOException", e);
1264 }
1265 return false;
1266
1267 } catch (Exception e) {
1268 if (V || Config.LOGD) {
1269 Log.d(TAG, "DnsPinger.isReachable got an unknown exception", e);
1270 }
1271 return false;
1272 }
1273 }
1274
1275 private static void fillQuery(byte[] buf) {
1276
1277 /*
1278 * See RFC2929 (though the bit tables in there are misleading for
1279 * us. For example, the recursion desired bit is the 0th bit for us,
1280 * but looking there it would appear as the 7th bit of the byte
1281 */
1282
1283 // Make sure it's all zeroed out
1284 for (int i = 0; i < buf.length; i++) buf[i] = 0;
1285
1286 // Form a query for www.android.com
1287
1288 // [0-1] bytes are an ID, generate random ID for this query
1289 buf[0] = (byte) sRandom.nextInt(256);
1290 buf[1] = (byte) sRandom.nextInt(256);
1291
1292 // [2-3] bytes are for flags.
1293 buf[2] = 1; // Recursion desired
1294
1295 // [4-5] bytes are for the query count
1296 buf[5] = 1; // One query
1297
1298 // [6-7] [8-9] [10-11] are all counts of other fields we don't use
1299
1300 // [12-15] for www
1301 writeString(buf, 12, "www");
1302
1303 // [16-23] for android
1304 writeString(buf, 16, "android");
1305
1306 // [24-27] for com
1307 writeString(buf, 24, "com");
1308
1309 // [29-30] bytes are for QTYPE, set to 1
1310 buf[30] = 1;
1311
1312 // [31-32] bytes are for QCLASS, set to 1
1313 buf[32] = 1;
1314 }
1315
1316 private static void writeString(byte[] buf, int startPos, String string) {
1317 int pos = startPos;
1318
1319 // Write the length first
1320 buf[pos++] = (byte) string.length();
1321 for (int i = 0; i < string.length(); i++) {
1322 buf[pos++] = (byte) string.charAt(i);
1323 }
1324 }
1325 }
1326}