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