NetworkMonitor.java revision fb68f8fbe0213841f393f8bdb5313e4e44f4f116
1/*
2 * Copyright (C) 2014 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.connectivity;
18
19import android.app.PendingIntent;
20import android.content.BroadcastReceiver;
21import android.content.ComponentName;
22import android.content.Context;
23import android.content.Intent;
24import android.content.IntentFilter;
25import android.net.ConnectivityManager;
26import android.net.Network;
27import android.net.NetworkCapabilities;
28import android.net.NetworkInfo;
29import android.net.wifi.WifiInfo;
30import android.net.wifi.WifiManager;
31import android.os.Handler;
32import android.os.Message;
33import android.os.SystemClock;
34import android.os.SystemProperties;
35import android.os.UserHandle;
36import android.provider.Settings;
37import android.telephony.CellIdentityCdma;
38import android.telephony.CellIdentityGsm;
39import android.telephony.CellIdentityLte;
40import android.telephony.CellIdentityWcdma;
41import android.telephony.CellInfo;
42import android.telephony.CellInfoCdma;
43import android.telephony.CellInfoGsm;
44import android.telephony.CellInfoLte;
45import android.telephony.CellInfoWcdma;
46import android.telephony.TelephonyManager;
47
48import com.android.internal.util.Protocol;
49import com.android.internal.util.State;
50import com.android.internal.util.StateMachine;
51import com.android.server.ConnectivityService;
52import com.android.server.connectivity.NetworkAgentInfo;
53
54import java.io.IOException;
55import java.net.HttpURLConnection;
56import java.net.URL;
57import java.util.List;
58
59/**
60 * {@hide}
61 */
62public class NetworkMonitor extends StateMachine {
63    private static final boolean DBG = true;
64    private static final String TAG = "NetworkMonitor";
65    private static final String DEFAULT_SERVER = "clients3.google.com";
66    private static final int SOCKET_TIMEOUT_MS = 10000;
67    public static final String ACTION_NETWORK_CONDITIONS_MEASURED =
68            "android.net.conn.NETWORK_CONDITIONS_MEASURED";
69    public static final String EXTRA_CONNECTIVITY_TYPE = "extra_connectivity_type";
70    public static final String EXTRA_NETWORK_TYPE = "extra_network_type";
71    public static final String EXTRA_RESPONSE_RECEIVED = "extra_response_received";
72    public static final String EXTRA_IS_CAPTIVE_PORTAL = "extra_is_captive_portal";
73    public static final String EXTRA_CELL_ID = "extra_cellid";
74    public static final String EXTRA_SSID = "extra_ssid";
75    public static final String EXTRA_BSSID = "extra_bssid";
76    /** real time since boot */
77    public static final String EXTRA_REQUEST_TIMESTAMP_MS = "extra_request_timestamp_ms";
78    public static final String EXTRA_RESPONSE_TIMESTAMP_MS = "extra_response_timestamp_ms";
79
80    private static final String PERMISSION_ACCESS_NETWORK_CONDITIONS =
81            "android.permission.ACCESS_NETWORK_CONDITIONS";
82
83    // Intent broadcast when user selects sign-in notification.
84    private static final String ACTION_SIGN_IN_REQUESTED =
85            "android.net.netmon.sign_in_requested";
86
87    // Keep these in sync with CaptivePortalLoginActivity.java.
88    // Intent broadcast from CaptivePortalLogin indicating sign-in is complete.
89    // Extras:
90    //     EXTRA_TEXT       = netId
91    //     LOGGED_IN_RESULT = "1" if we should use network, "0" if not.
92    private static final String ACTION_CAPTIVE_PORTAL_LOGGED_IN =
93            "android.net.netmon.captive_portal_logged_in";
94    private static final String LOGGED_IN_RESULT = "result";
95
96    private static final int BASE = Protocol.BASE_NETWORK_MONITOR;
97
98    /**
99     * Inform NetworkMonitor that their network is connected.
100     * Initiates Network Validation.
101     */
102    public static final int CMD_NETWORK_CONNECTED = BASE + 1;
103
104    /**
105     * Inform ConnectivityService that the network is validated.
106     * obj = NetworkAgentInfo
107     */
108    public static final int EVENT_NETWORK_VALIDATED = BASE + 2;
109
110    /**
111     * Inform NetworkMonitor to linger a network.  The Monitor should
112     * start a timer and/or start watching for zero live connections while
113     * moving towards LINGER_COMPLETE.  After the Linger period expires
114     * (or other events mark the end of the linger state) the LINGER_COMPLETE
115     * event should be sent and the network will be shut down.  If a
116     * CMD_NETWORK_CONNECTED happens before the LINGER completes
117     * it indicates further desire to keep the network alive and so
118     * the LINGER is aborted.
119     */
120    public static final int CMD_NETWORK_LINGER = BASE + 3;
121
122    /**
123     * Message to self indicating linger delay has expired.
124     * arg1 = Token to ignore old messages.
125     */
126    private static final int CMD_LINGER_EXPIRED = BASE + 4;
127
128    /**
129     * Inform ConnectivityService that the network LINGER period has
130     * expired.
131     * obj = NetworkAgentInfo
132     */
133    public static final int EVENT_NETWORK_LINGER_COMPLETE = BASE + 5;
134
135    /**
136     * Message to self indicating it's time to evaluate a network's connectivity.
137     * arg1 = Token to ignore old messages.
138     */
139    private static final int CMD_REEVALUATE = BASE + 6;
140
141    /**
142     * Message to self indicating network evaluation is complete.
143     * arg1 = Token to ignore old messages.
144     * arg2 = HTTP response code of network evaluation.
145     */
146    private static final int EVENT_REEVALUATION_COMPLETE = BASE + 7;
147
148    /**
149     * Inform NetworkMonitor that the network has disconnected.
150     */
151    public static final int CMD_NETWORK_DISCONNECTED = BASE + 8;
152
153    /**
154     * Force evaluation even if it has succeeded in the past.
155     */
156    public static final int CMD_FORCE_REEVALUATION = BASE + 9;
157
158    /**
159     * Message to self indicating captive portal login is complete.
160     * arg1 = Token to ignore old messages.
161     * arg2 = 1 if we should use this network, 0 otherwise.
162     */
163    private static final int CMD_CAPTIVE_PORTAL_LOGGED_IN = BASE + 10;
164
165    /**
166     * Message to self indicating user desires to log into captive portal.
167     * arg1 = Token to ignore old messages.
168     */
169    private static final int CMD_USER_WANTS_SIGN_IN = BASE + 11;
170
171    /**
172     * Request ConnectivityService display provisioning notification.
173     * arg1    = Whether to make the notification visible.
174     * arg2    = NetID.
175     * obj     = Intent to be launched when notification selected by user, null if !arg1.
176     */
177    public static final int EVENT_PROVISIONING_NOTIFICATION = BASE + 12;
178
179    /**
180     * Message to self indicating sign-in app bypassed captive portal.
181     */
182    private static final int EVENT_APP_BYPASSED_CAPTIVE_PORTAL = BASE + 13;
183
184    /**
185     * Message to self indicating no sign-in app responded.
186     */
187    private static final int EVENT_NO_APP_RESPONSE = BASE + 14;
188
189    /**
190     * Message to self indicating sign-in app indicates sign-in is not possible.
191     */
192    private static final int EVENT_APP_INDICATES_SIGN_IN_IMPOSSIBLE = BASE + 15;
193
194    private static final String LINGER_DELAY_PROPERTY = "persist.netmon.linger";
195    // Default to 30s linger time-out.
196    private static final int DEFAULT_LINGER_DELAY_MS = 30000;
197    private final int mLingerDelayMs;
198    private int mLingerToken = 0;
199
200    // Negative values disable reevaluation.
201    private static final String REEVALUATE_DELAY_PROPERTY = "persist.netmon.reeval_delay";
202    // Default to 5s reevaluation delay.
203    private static final int DEFAULT_REEVALUATE_DELAY_MS = 5000;
204    private static final int MAX_RETRIES = 10;
205    private final int mReevaluateDelayMs;
206    private int mReevaluateToken = 0;
207
208    private int mCaptivePortalLoggedInToken = 0;
209    private int mUserPromptedToken = 0;
210
211    private final Context mContext;
212    private final Handler mConnectivityServiceHandler;
213    private final NetworkAgentInfo mNetworkAgentInfo;
214    private final TelephonyManager mTelephonyManager;
215    private final WifiManager mWifiManager;
216
217    private String mServer;
218    private boolean mIsCaptivePortalCheckEnabled = false;
219
220    public boolean systemReady = false;
221
222    private State mDefaultState = new DefaultState();
223    private State mOfflineState = new OfflineState();
224    private State mValidatedState = new ValidatedState();
225    private State mEvaluatingState = new EvaluatingState();
226    private State mUninteractiveAppsPromptedState = new UninteractiveAppsPromptedState();
227    private State mUserPromptedState = new UserPromptedState();
228    private State mInteractiveAppsPromptedState = new InteractiveAppsPromptedState();
229    private State mCaptivePortalState = new CaptivePortalState();
230    private State mLingeringState = new LingeringState();
231
232    public NetworkMonitor(Context context, Handler handler, NetworkAgentInfo networkAgentInfo) {
233        // Add suffix indicating which NetworkMonitor we're talking about.
234        super(TAG + networkAgentInfo.name());
235
236        mContext = context;
237        mConnectivityServiceHandler = handler;
238        mNetworkAgentInfo = networkAgentInfo;
239        mTelephonyManager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
240        mWifiManager = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
241
242        addState(mDefaultState);
243        addState(mOfflineState, mDefaultState);
244        addState(mValidatedState, mDefaultState);
245        addState(mEvaluatingState, mDefaultState);
246        addState(mUninteractiveAppsPromptedState, mDefaultState);
247        addState(mUserPromptedState, mDefaultState);
248        addState(mInteractiveAppsPromptedState, mDefaultState);
249        addState(mCaptivePortalState, mDefaultState);
250        addState(mLingeringState, mDefaultState);
251        setInitialState(mOfflineState);
252
253        mServer = Settings.Global.getString(mContext.getContentResolver(),
254                Settings.Global.CAPTIVE_PORTAL_SERVER);
255        if (mServer == null) mServer = DEFAULT_SERVER;
256
257        mLingerDelayMs = SystemProperties.getInt(LINGER_DELAY_PROPERTY, DEFAULT_LINGER_DELAY_MS);
258        mReevaluateDelayMs = SystemProperties.getInt(REEVALUATE_DELAY_PROPERTY,
259                DEFAULT_REEVALUATE_DELAY_MS);
260
261        mIsCaptivePortalCheckEnabled = Settings.Global.getInt(mContext.getContentResolver(),
262                Settings.Global.CAPTIVE_PORTAL_DETECTION_ENABLED, 1) == 1;
263
264        start();
265    }
266
267    private class DefaultState extends State {
268        @Override
269        public boolean processMessage(Message message) {
270            if (DBG) log(getName() + message.toString());
271            switch (message.what) {
272                case CMD_NETWORK_LINGER:
273                    if (DBG) log("Lingering");
274                    transitionTo(mLingeringState);
275                    break;
276                case CMD_NETWORK_CONNECTED:
277                    if (DBG) log("Connected");
278                    transitionTo(mEvaluatingState);
279                    break;
280                case CMD_NETWORK_DISCONNECTED:
281                    if (DBG) log("Disconnected - quitting");
282                    quit();
283                    break;
284                case CMD_FORCE_REEVALUATION:
285                    if (DBG) log("Forcing reevaluation");
286                    transitionTo(mEvaluatingState);
287                    break;
288                default:
289                    break;
290            }
291            return HANDLED;
292        }
293    }
294
295    private class OfflineState extends State {
296        @Override
297        public boolean processMessage(Message message) {
298            if (DBG) log(getName() + message.toString());
299            return NOT_HANDLED;
300        }
301    }
302
303    private class ValidatedState extends State {
304        @Override
305        public void enter() {
306            if (DBG) log("Validated");
307            mConnectivityServiceHandler.sendMessage(
308                    obtainMessage(EVENT_NETWORK_VALIDATED, mNetworkAgentInfo));
309        }
310
311        @Override
312        public boolean processMessage(Message message) {
313            if (DBG) log(getName() + message.toString());
314            switch (message.what) {
315                case CMD_NETWORK_CONNECTED:
316                    transitionTo(mValidatedState);
317                    break;
318                default:
319                    return NOT_HANDLED;
320            }
321            return HANDLED;
322        }
323    }
324
325    private class EvaluatingState extends State {
326        private int mRetries;
327
328        private class EvaluateInternetConnectivity extends Thread {
329            private int mToken;
330            EvaluateInternetConnectivity(int token) {
331                mToken = token;
332            }
333            public void run() {
334                sendMessage(EVENT_REEVALUATION_COMPLETE, mToken, isCaptivePortal());
335            }
336        }
337
338        @Override
339        public void enter() {
340            mRetries = 0;
341            sendMessage(CMD_REEVALUATE, ++mReevaluateToken, 0);
342        }
343
344        @Override
345        public boolean processMessage(Message message) {
346            if (DBG) log(getName() + message.toString());
347            switch (message.what) {
348                case CMD_REEVALUATE:
349                    if (message.arg1 != mReevaluateToken)
350                        break;
351                    if (mNetworkAgentInfo.isVPN()) {
352                        transitionTo(mValidatedState);
353                    }
354                    // If network provides no internet connectivity adjust evaluation.
355                    if (!mNetworkAgentInfo.networkCapabilities.hasCapability(
356                            NetworkCapabilities.NET_CAPABILITY_INTERNET)) {
357                        // TODO: Try to verify something works.  Do all gateways respond to pings?
358                        transitionTo(mValidatedState);
359                    }
360                    // Kick off a thread to perform internet connectivity evaluation.
361                    Thread thread = new EvaluateInternetConnectivity(mReevaluateToken);
362                    thread.run();
363                    break;
364                case EVENT_REEVALUATION_COMPLETE:
365                    if (message.arg1 != mReevaluateToken)
366                        break;
367                    int httpResponseCode = message.arg2;
368                    if (httpResponseCode == 204) {
369                        transitionTo(mValidatedState);
370                    } else if (httpResponseCode >= 200 && httpResponseCode <= 399) {
371                        transitionTo(mUninteractiveAppsPromptedState);
372                    } else if (++mRetries > MAX_RETRIES) {
373                        transitionTo(mOfflineState);
374                    } else if (mReevaluateDelayMs >= 0) {
375                        Message msg = obtainMessage(CMD_REEVALUATE, ++mReevaluateToken, 0);
376                        sendMessageDelayed(msg, mReevaluateDelayMs);
377                    }
378                    break;
379                default:
380                    return NOT_HANDLED;
381            }
382            return HANDLED;
383        }
384    }
385
386    private class AppRespondedBroadcastReceiver extends BroadcastReceiver {
387        private static final int CAPTIVE_PORTAL_UNINITIALIZED_RETURN_CODE = 0;
388        private boolean mCanceled;
389        AppRespondedBroadcastReceiver() {
390            mCanceled = false;
391        }
392        public void send(String action) {
393            Intent intent = new Intent(action);
394            intent.putExtra(ConnectivityManager.EXTRA_NETWORK, mNetworkAgentInfo.network);
395            mContext.sendOrderedBroadcastAsUser(intent, UserHandle.ALL, null, this, getHandler(),
396                    CAPTIVE_PORTAL_UNINITIALIZED_RETURN_CODE, null, null);
397        }
398        public void cancel() {
399            mCanceled = true;
400        }
401        @Override
402        public void onReceive(Context context, Intent intent) {
403            if (!mCanceled) {
404                cancel();
405                switch (getResultCode()) {
406                    case ConnectivityManager.CAPTIVE_PORTAL_SIGNED_IN:
407                        sendMessage(EVENT_APP_BYPASSED_CAPTIVE_PORTAL);
408                        break;
409                    case ConnectivityManager.CAPTIVE_PORTAL_DISCONNECT:
410                        sendMessage(EVENT_APP_INDICATES_SIGN_IN_IMPOSSIBLE);
411                        break;
412                    // NOTE: This case label makes compiler enforce no overlap between result codes.
413                    case CAPTIVE_PORTAL_UNINITIALIZED_RETURN_CODE:
414                    default:
415                        sendMessage(EVENT_NO_APP_RESPONSE);
416                        break;
417                }
418            }
419        }
420    }
421
422    private class UninteractiveAppsPromptedState extends State {
423        private AppRespondedBroadcastReceiver mReceiver;
424        @Override
425        public void enter() {
426            mReceiver = new AppRespondedBroadcastReceiver();
427            mReceiver.send(ConnectivityManager.ACTION_CAPTIVE_PORTAL_DETECTED);
428        }
429        @Override
430        public boolean processMessage(Message message) {
431            if (DBG) log(getName() + message.toString());
432            switch (message.what) {
433                case EVENT_APP_BYPASSED_CAPTIVE_PORTAL:
434                    transitionTo(mValidatedState);
435                    break;
436                case EVENT_APP_INDICATES_SIGN_IN_IMPOSSIBLE:
437                    transitionTo(mOfflineState);
438                    break;
439                case EVENT_NO_APP_RESPONSE:
440                    transitionTo(mUserPromptedState);
441                    break;
442                default:
443                    return NOT_HANDLED;
444            }
445            return HANDLED;
446        }
447        public void exit() {
448            mReceiver.cancel();
449        }
450    }
451
452    private class UserPromptedState extends State {
453        private class UserRespondedBroadcastReceiver extends BroadcastReceiver {
454            private final int mToken;
455            UserRespondedBroadcastReceiver(int token) {
456                mToken = token;
457            }
458            @Override
459            public void onReceive(Context context, Intent intent) {
460                if (Integer.parseInt(intent.getStringExtra(Intent.EXTRA_TEXT)) ==
461                        mNetworkAgentInfo.network.netId) {
462                    sendMessage(obtainMessage(CMD_USER_WANTS_SIGN_IN, mToken));
463                }
464            }
465        }
466
467        private UserRespondedBroadcastReceiver mUserRespondedBroadcastReceiver;
468
469        @Override
470        public void enter() {
471            // Wait for user to select sign-in notifcation.
472            mUserRespondedBroadcastReceiver = new UserRespondedBroadcastReceiver(
473                    ++mUserPromptedToken);
474            IntentFilter filter = new IntentFilter(ACTION_SIGN_IN_REQUESTED);
475            mContext.registerReceiver(mUserRespondedBroadcastReceiver, filter);
476            // Initiate notification to sign-in.
477            Intent intent = new Intent(ACTION_SIGN_IN_REQUESTED);
478            intent.putExtra(Intent.EXTRA_TEXT, String.valueOf(mNetworkAgentInfo.network.netId));
479            Message message = obtainMessage(EVENT_PROVISIONING_NOTIFICATION, 1,
480                    mNetworkAgentInfo.network.netId,
481                    PendingIntent.getBroadcast(mContext, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT));
482            mConnectivityServiceHandler.sendMessage(message);
483        }
484
485        @Override
486        public boolean processMessage(Message message) {
487            if (DBG) log(getName() + message.toString());
488            switch (message.what) {
489                case CMD_USER_WANTS_SIGN_IN:
490                    if (message.arg1 != mUserPromptedToken)
491                        break;
492                    transitionTo(mInteractiveAppsPromptedState);
493                    break;
494                default:
495                    return NOT_HANDLED;
496            }
497            return HANDLED;
498        }
499
500        @Override
501        public void exit() {
502            Message message = obtainMessage(EVENT_PROVISIONING_NOTIFICATION, 0,
503                    mNetworkAgentInfo.network.netId, null);
504            mConnectivityServiceHandler.sendMessage(message);
505            mContext.unregisterReceiver(mUserRespondedBroadcastReceiver);
506            mUserRespondedBroadcastReceiver = null;
507        }
508    }
509
510    private class InteractiveAppsPromptedState extends State {
511        private AppRespondedBroadcastReceiver mReceiver;
512        @Override
513        public void enter() {
514            mReceiver = new AppRespondedBroadcastReceiver();
515            mReceiver.send(ConnectivityManager.ACTION_CAPTIVE_PORTAL_SIGN_IN);
516        }
517        @Override
518        public boolean processMessage(Message message) {
519            if (DBG) log(getName() + message.toString());
520            switch (message.what) {
521                case EVENT_APP_BYPASSED_CAPTIVE_PORTAL:
522                    transitionTo(mValidatedState);
523                    break;
524                case EVENT_APP_INDICATES_SIGN_IN_IMPOSSIBLE:
525                    transitionTo(mOfflineState);
526                    break;
527                case EVENT_NO_APP_RESPONSE:
528                    transitionTo(mCaptivePortalState);
529                    break;
530                default:
531                    return NOT_HANDLED;
532            }
533            return HANDLED;
534        }
535        public void exit() {
536            mReceiver.cancel();
537        }
538    }
539
540    private class CaptivePortalState extends State {
541        private class CaptivePortalLoggedInBroadcastReceiver extends BroadcastReceiver {
542            private final int mToken;
543
544            CaptivePortalLoggedInBroadcastReceiver(int token) {
545                mToken = token;
546            }
547
548            @Override
549            public void onReceive(Context context, Intent intent) {
550                if (Integer.parseInt(intent.getStringExtra(Intent.EXTRA_TEXT)) ==
551                        mNetworkAgentInfo.network.netId) {
552                    sendMessage(obtainMessage(CMD_CAPTIVE_PORTAL_LOGGED_IN, mToken,
553                            Integer.parseInt(intent.getStringExtra(LOGGED_IN_RESULT))));
554                }
555            }
556        }
557
558        private CaptivePortalLoggedInBroadcastReceiver mCaptivePortalLoggedInBroadcastReceiver;
559
560        @Override
561        public void enter() {
562            Intent intent = new Intent(Intent.ACTION_SEND);
563            intent.putExtra(Intent.EXTRA_TEXT, String.valueOf(mNetworkAgentInfo.network.netId));
564            intent.setType("text/plain");
565            intent.setComponent(new ComponentName("com.android.captiveportallogin",
566                    "com.android.captiveportallogin.CaptivePortalLoginActivity"));
567            intent.setFlags(Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT | Intent.FLAG_ACTIVITY_NEW_TASK);
568
569            // Wait for result.
570            mCaptivePortalLoggedInBroadcastReceiver = new CaptivePortalLoggedInBroadcastReceiver(
571                    ++mCaptivePortalLoggedInToken);
572            IntentFilter filter = new IntentFilter(ACTION_CAPTIVE_PORTAL_LOGGED_IN);
573            mContext.registerReceiver(mCaptivePortalLoggedInBroadcastReceiver, filter);
574            // Initiate app to log in.
575            mContext.startActivityAsUser(intent, UserHandle.CURRENT);
576        }
577
578        @Override
579        public boolean processMessage(Message message) {
580            if (DBG) log(getName() + message.toString());
581            switch (message.what) {
582                case CMD_CAPTIVE_PORTAL_LOGGED_IN:
583                    if (message.arg1 != mCaptivePortalLoggedInToken)
584                        break;
585                    if (message.arg2 == 0) {
586                        // TODO: Should teardown network.
587                        transitionTo(mOfflineState);
588                    } else {
589                        transitionTo(mValidatedState);
590                    }
591                    break;
592                default:
593                    return NOT_HANDLED;
594            }
595            return HANDLED;
596        }
597
598        @Override
599        public void exit() {
600            mContext.unregisterReceiver(mCaptivePortalLoggedInBroadcastReceiver);
601            mCaptivePortalLoggedInBroadcastReceiver = null;
602        }
603    }
604
605    private class LingeringState extends State {
606        @Override
607        public void enter() {
608            Message message = obtainMessage(CMD_LINGER_EXPIRED, ++mLingerToken, 0);
609            sendMessageDelayed(message, mLingerDelayMs);
610        }
611
612        @Override
613        public boolean processMessage(Message message) {
614            if (DBG) log(getName() + message.toString());
615            switch (message.what) {
616                case CMD_NETWORK_CONNECTED:
617                    // Go straight to active as we've already evaluated.
618                    transitionTo(mValidatedState);
619                    break;
620                case CMD_LINGER_EXPIRED:
621                    if (message.arg1 != mLingerToken)
622                        break;
623                    mConnectivityServiceHandler.sendMessage(
624                            obtainMessage(EVENT_NETWORK_LINGER_COMPLETE, mNetworkAgentInfo));
625                    break;
626                default:
627                    return NOT_HANDLED;
628            }
629            return HANDLED;
630        }
631    }
632
633    /**
634     * Do a URL fetch on a known server to see if we get the data we expect.
635     * Returns HTTP response code.
636     */
637    private int isCaptivePortal() {
638        if (!mIsCaptivePortalCheckEnabled) return 204;
639
640        HttpURLConnection urlConnection = null;
641        int httpResponseCode = 599;
642        try {
643            URL url = new URL("http", mServer, "/generate_204");
644            if (DBG) {
645                log("Checking " + url.toString() + " on " +
646                        mNetworkAgentInfo.networkInfo.getExtraInfo());
647            }
648            url = mNetworkAgentInfo.network.getBoundURL(url);
649            urlConnection = (HttpURLConnection) url.openConnection();
650            urlConnection.setInstanceFollowRedirects(false);
651            urlConnection.setConnectTimeout(SOCKET_TIMEOUT_MS);
652            urlConnection.setReadTimeout(SOCKET_TIMEOUT_MS);
653            urlConnection.setUseCaches(false);
654
655            // Time how long it takes to get a response to our request
656            long requestTimestamp = SystemClock.elapsedRealtime();
657
658            urlConnection.getInputStream();
659
660            // Time how long it takes to get a response to our request
661            long responseTimestamp = SystemClock.elapsedRealtime();
662
663            httpResponseCode = urlConnection.getResponseCode();
664            if (DBG) {
665                log("isCaptivePortal: ret=" + httpResponseCode +
666                        " headers=" + urlConnection.getHeaderFields());
667            }
668            // NOTE: We may want to consider an "HTTP/1.0 204" response to be a captive
669            // portal.  The only example of this seen so far was a captive portal.  For
670            // the time being go with prior behavior of assuming it's not a captive
671            // portal.  If it is considered a captive portal, a different sign-in URL
672            // is needed (i.e. can't browse a 204).  This could be the result of an HTTP
673            // proxy server.
674
675            // Consider 200 response with "Content-length=0" to not be a captive portal.
676            // There's no point in considering this a captive portal as the user cannot
677            // sign-in to an empty page.  Probably the result of a broken transparent proxy.
678            // See http://b/9972012.
679            if (httpResponseCode == 200 && urlConnection.getContentLength() == 0) {
680                if (DBG) log("Empty 200 response interpreted as 204 response.");
681                httpResponseCode = 204;
682            }
683
684            sendNetworkConditionsBroadcast(true /* response received */, httpResponseCode == 204,
685                    requestTimestamp, responseTimestamp);
686        } catch (IOException e) {
687            if (DBG) log("Probably not a portal: exception " + e);
688            if (httpResponseCode == 599) {
689                // TODO: Ping gateway and DNS server and log results.
690            }
691        } finally {
692            if (urlConnection != null) {
693                urlConnection.disconnect();
694            }
695        }
696        return httpResponseCode;
697    }
698
699    /**
700     * @param responseReceived - whether or not we received a valid HTTP response to our request.
701     * If false, isCaptivePortal and responseTimestampMs are ignored
702     * TODO: This should be moved to the transports.  The latency could be passed to the transports
703     * along with the captive portal result.  Currently the TYPE_MOBILE broadcasts appear unused so
704     * perhaps this could just be added to the WiFi transport only.
705     */
706    private void sendNetworkConditionsBroadcast(boolean responseReceived, boolean isCaptivePortal,
707            long requestTimestampMs, long responseTimestampMs) {
708        if (Settings.Global.getInt(mContext.getContentResolver(),
709                Settings.Global.WIFI_SCAN_ALWAYS_AVAILABLE, 0) == 0) {
710            if (DBG) log("Don't send network conditions - lacking user consent.");
711            return;
712        }
713
714        if (systemReady == false) return;
715
716        Intent latencyBroadcast = new Intent(ACTION_NETWORK_CONDITIONS_MEASURED);
717        switch (mNetworkAgentInfo.networkInfo.getType()) {
718            case ConnectivityManager.TYPE_WIFI:
719                WifiInfo currentWifiInfo = mWifiManager.getConnectionInfo();
720                if (currentWifiInfo != null) {
721                    // NOTE: getSSID()'s behavior changed in API 17; before that, SSIDs were not
722                    // surrounded by double quotation marks (thus violating the Javadoc), but this
723                    // was changed to match the Javadoc in API 17. Since clients may have started
724                    // sanitizing the output of this method since API 17 was released, we should
725                    // not change it here as it would become impossible to tell whether the SSID is
726                    // simply being surrounded by quotes due to the API, or whether those quotes
727                    // are actually part of the SSID.
728                    latencyBroadcast.putExtra(EXTRA_SSID, currentWifiInfo.getSSID());
729                    latencyBroadcast.putExtra(EXTRA_BSSID, currentWifiInfo.getBSSID());
730                } else {
731                    if (DBG) logw("network info is TYPE_WIFI but no ConnectionInfo found");
732                    return;
733                }
734                break;
735            case ConnectivityManager.TYPE_MOBILE:
736                latencyBroadcast.putExtra(EXTRA_NETWORK_TYPE, mTelephonyManager.getNetworkType());
737                List<CellInfo> info = mTelephonyManager.getAllCellInfo();
738                if (info == null) return;
739                int numRegisteredCellInfo = 0;
740                for (CellInfo cellInfo : info) {
741                    if (cellInfo.isRegistered()) {
742                        numRegisteredCellInfo++;
743                        if (numRegisteredCellInfo > 1) {
744                            if (DBG) log("more than one registered CellInfo.  Can't " +
745                                    "tell which is active.  Bailing.");
746                            return;
747                        }
748                        if (cellInfo instanceof CellInfoCdma) {
749                            CellIdentityCdma cellId = ((CellInfoCdma) cellInfo).getCellIdentity();
750                            latencyBroadcast.putExtra(EXTRA_CELL_ID, cellId);
751                        } else if (cellInfo instanceof CellInfoGsm) {
752                            CellIdentityGsm cellId = ((CellInfoGsm) cellInfo).getCellIdentity();
753                            latencyBroadcast.putExtra(EXTRA_CELL_ID, cellId);
754                        } else if (cellInfo instanceof CellInfoLte) {
755                            CellIdentityLte cellId = ((CellInfoLte) cellInfo).getCellIdentity();
756                            latencyBroadcast.putExtra(EXTRA_CELL_ID, cellId);
757                        } else if (cellInfo instanceof CellInfoWcdma) {
758                            CellIdentityWcdma cellId = ((CellInfoWcdma) cellInfo).getCellIdentity();
759                            latencyBroadcast.putExtra(EXTRA_CELL_ID, cellId);
760                        } else {
761                            if (DBG) logw("Registered cellinfo is unrecognized");
762                            return;
763                        }
764                    }
765                }
766                break;
767            default:
768                return;
769        }
770        latencyBroadcast.putExtra(EXTRA_CONNECTIVITY_TYPE, mNetworkAgentInfo.networkInfo.getType());
771        latencyBroadcast.putExtra(EXTRA_RESPONSE_RECEIVED, responseReceived);
772        latencyBroadcast.putExtra(EXTRA_REQUEST_TIMESTAMP_MS, requestTimestampMs);
773
774        if (responseReceived) {
775            latencyBroadcast.putExtra(EXTRA_IS_CAPTIVE_PORTAL, isCaptivePortal);
776            latencyBroadcast.putExtra(EXTRA_RESPONSE_TIMESTAMP_MS, responseTimestampMs);
777        }
778        mContext.sendBroadcast(latencyBroadcast, PERMISSION_ACCESS_NETWORK_CONDITIONS);
779    }
780}
781