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