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 static android.net.CaptivePortal.APP_RETURN_DISMISSED;
     20 import static android.net.CaptivePortal.APP_RETURN_UNWANTED;
     21 import static android.net.CaptivePortal.APP_RETURN_WANTED_AS_IS;
     22 
     23 import android.app.AlarmManager;
     24 import android.app.PendingIntent;
     25 import android.content.BroadcastReceiver;
     26 import android.content.Context;
     27 import android.content.Intent;
     28 import android.content.IntentFilter;
     29 import android.net.CaptivePortal;
     30 import android.net.ConnectivityManager;
     31 import android.net.ICaptivePortal;
     32 import android.net.NetworkRequest;
     33 import android.net.ProxyInfo;
     34 import android.net.TrafficStats;
     35 import android.net.Uri;
     36 import android.net.metrics.IpConnectivityLog;
     37 import android.net.metrics.NetworkEvent;
     38 import android.net.metrics.ValidationProbeEvent;
     39 import android.net.util.Stopwatch;
     40 import android.net.wifi.WifiInfo;
     41 import android.net.wifi.WifiManager;
     42 import android.os.Handler;
     43 import android.os.Message;
     44 import android.os.SystemClock;
     45 import android.os.UserHandle;
     46 import android.provider.Settings;
     47 import android.telephony.CellIdentityCdma;
     48 import android.telephony.CellIdentityGsm;
     49 import android.telephony.CellIdentityLte;
     50 import android.telephony.CellIdentityWcdma;
     51 import android.telephony.CellInfo;
     52 import android.telephony.CellInfoCdma;
     53 import android.telephony.CellInfoGsm;
     54 import android.telephony.CellInfoLte;
     55 import android.telephony.CellInfoWcdma;
     56 import android.telephony.TelephonyManager;
     57 import android.text.TextUtils;
     58 import android.util.LocalLog;
     59 import android.util.LocalLog.ReadOnlyLocalLog;
     60 import android.util.Log;
     61 
     62 import com.android.internal.annotations.VisibleForTesting;
     63 import com.android.internal.util.Protocol;
     64 import com.android.internal.util.State;
     65 import com.android.internal.util.StateMachine;
     66 
     67 import java.io.IOException;
     68 import java.net.HttpURLConnection;
     69 import java.net.InetAddress;
     70 import java.net.MalformedURLException;
     71 import java.net.URL;
     72 import java.net.UnknownHostException;
     73 import java.util.List;
     74 import java.util.Random;
     75 import java.util.concurrent.CountDownLatch;
     76 import java.util.concurrent.TimeUnit;
     77 
     78 /**
     79  * {@hide}
     80  */
     81 public class NetworkMonitor extends StateMachine {
     82     private static final String TAG = NetworkMonitor.class.getSimpleName();
     83     private static final boolean DBG = false;
     84 
     85     // Default configuration values for captive portal detection probes.
     86     // TODO: append a random length parameter to the default HTTPS url.
     87     // TODO: randomize browser version ids in the default User-Agent String.
     88     private static final String DEFAULT_HTTPS_URL     = "https://www.google.com/generate_204";
     89     private static final String DEFAULT_HTTP_URL      =
     90             "http://connectivitycheck.gstatic.com/generate_204";
     91     private static final String DEFAULT_FALLBACK_URL  = "http://www.google.com/gen_204";
     92     private static final String DEFAULT_USER_AGENT    = "Mozilla/5.0 (X11; Linux x86_64) "
     93                                                       + "AppleWebKit/537.36 (KHTML, like Gecko) "
     94                                                       + "Chrome/52.0.2743.82 Safari/537.36";
     95 
     96     private static final int SOCKET_TIMEOUT_MS = 10000;
     97     private static final int PROBE_TIMEOUT_MS  = 3000;
     98 
     99     public static final String ACTION_NETWORK_CONDITIONS_MEASURED =
    100             "android.net.conn.NETWORK_CONDITIONS_MEASURED";
    101     public static final String EXTRA_CONNECTIVITY_TYPE = "extra_connectivity_type";
    102     public static final String EXTRA_NETWORK_TYPE = "extra_network_type";
    103     public static final String EXTRA_RESPONSE_RECEIVED = "extra_response_received";
    104     public static final String EXTRA_IS_CAPTIVE_PORTAL = "extra_is_captive_portal";
    105     public static final String EXTRA_CELL_ID = "extra_cellid";
    106     public static final String EXTRA_SSID = "extra_ssid";
    107     public static final String EXTRA_BSSID = "extra_bssid";
    108     /** real time since boot */
    109     public static final String EXTRA_REQUEST_TIMESTAMP_MS = "extra_request_timestamp_ms";
    110     public static final String EXTRA_RESPONSE_TIMESTAMP_MS = "extra_response_timestamp_ms";
    111 
    112     private static final String PERMISSION_ACCESS_NETWORK_CONDITIONS =
    113             "android.permission.ACCESS_NETWORK_CONDITIONS";
    114 
    115     // After a network has been tested this result can be sent with EVENT_NETWORK_TESTED.
    116     // The network should be used as a default internet connection.  It was found to be:
    117     // 1. a functioning network providing internet access, or
    118     // 2. a captive portal and the user decided to use it as is.
    119     public static final int NETWORK_TEST_RESULT_VALID = 0;
    120     // After a network has been tested this result can be sent with EVENT_NETWORK_TESTED.
    121     // The network should not be used as a default internet connection.  It was found to be:
    122     // 1. a captive portal and the user is prompted to sign-in, or
    123     // 2. a captive portal and the user did not want to use it, or
    124     // 3. a broken network (e.g. DNS failed, connect failed, HTTP request failed).
    125     public static final int NETWORK_TEST_RESULT_INVALID = 1;
    126 
    127     private static final int BASE = Protocol.BASE_NETWORK_MONITOR;
    128 
    129     /**
    130      * Inform NetworkMonitor that their network is connected.
    131      * Initiates Network Validation.
    132      */
    133     public static final int CMD_NETWORK_CONNECTED = BASE + 1;
    134 
    135     /**
    136      * Inform ConnectivityService that the network has been tested.
    137      * obj = String representing URL that Internet probe was redirect to, if it was redirected.
    138      * arg1 = One of the NETWORK_TESTED_RESULT_* constants.
    139      * arg2 = NetID.
    140      */
    141     public static final int EVENT_NETWORK_TESTED = BASE + 2;
    142 
    143     /**
    144      * Message to self indicating it's time to evaluate a network's connectivity.
    145      * arg1 = Token to ignore old messages.
    146      */
    147     private static final int CMD_REEVALUATE = BASE + 6;
    148 
    149     /**
    150      * Inform NetworkMonitor that the network has disconnected.
    151      */
    152     public static final int CMD_NETWORK_DISCONNECTED = BASE + 7;
    153 
    154     /**
    155      * Force evaluation even if it has succeeded in the past.
    156      * arg1 = UID responsible for requesting this reeval.  Will be billed for data.
    157      */
    158     public static final int CMD_FORCE_REEVALUATION = BASE + 8;
    159 
    160     /**
    161      * Message to self indicating captive portal app finished.
    162      * arg1 = one of: APP_RETURN_DISMISSED,
    163      *                APP_RETURN_UNWANTED,
    164      *                APP_RETURN_WANTED_AS_IS
    165      * obj = mCaptivePortalLoggedInResponseToken as String
    166      */
    167     private static final int CMD_CAPTIVE_PORTAL_APP_FINISHED = BASE + 9;
    168 
    169     /**
    170      * Request ConnectivityService display provisioning notification.
    171      * arg1    = Whether to make the notification visible.
    172      * arg2    = NetID.
    173      * obj     = Intent to be launched when notification selected by user, null if !arg1.
    174      */
    175     public static final int EVENT_PROVISIONING_NOTIFICATION = BASE + 10;
    176 
    177     /**
    178      * Message to self indicating sign-in app should be launched.
    179      * Sent by mLaunchCaptivePortalAppBroadcastReceiver when the
    180      * user touches the sign in notification.
    181      */
    182     private static final int CMD_LAUNCH_CAPTIVE_PORTAL_APP = BASE + 11;
    183 
    184     /**
    185      * Retest network to see if captive portal is still in place.
    186      * arg1 = UID responsible for requesting this reeval.  Will be billed for data.
    187      *        0 indicates self-initiated, so nobody to blame.
    188      */
    189     private static final int CMD_CAPTIVE_PORTAL_RECHECK = BASE + 12;
    190 
    191     // Start mReevaluateDelayMs at this value and double.
    192     private static final int INITIAL_REEVALUATE_DELAY_MS = 1000;
    193     private static final int MAX_REEVALUATE_DELAY_MS = 10*60*1000;
    194     // Before network has been evaluated this many times, ignore repeated reevaluate requests.
    195     private static final int IGNORE_REEVALUATE_ATTEMPTS = 5;
    196     private int mReevaluateToken = 0;
    197     private static final int INVALID_UID = -1;
    198     private int mUidResponsibleForReeval = INVALID_UID;
    199     // Stop blaming UID that requested re-evaluation after this many attempts.
    200     private static final int BLAME_FOR_EVALUATION_ATTEMPTS = 5;
    201     // Delay between reevaluations once a captive portal has been found.
    202     private static final int CAPTIVE_PORTAL_REEVALUATE_DELAY_MS = 10*60*1000;
    203 
    204     private final Context mContext;
    205     private final Handler mConnectivityServiceHandler;
    206     private final NetworkAgentInfo mNetworkAgentInfo;
    207     private final int mNetId;
    208     private final TelephonyManager mTelephonyManager;
    209     private final WifiManager mWifiManager;
    210     private final AlarmManager mAlarmManager;
    211     private final NetworkRequest mDefaultRequest;
    212     private final IpConnectivityLog mMetricsLog;
    213 
    214     private boolean mIsCaptivePortalCheckEnabled;
    215     private boolean mUseHttps;
    216 
    217     // Set if the user explicitly selected "Do not use this network" in captive portal sign-in app.
    218     private boolean mUserDoesNotWant = false;
    219     // Avoids surfacing "Sign in to network" notification.
    220     private boolean mDontDisplaySigninNotification = false;
    221 
    222     public boolean systemReady = false;
    223 
    224     private final State mDefaultState = new DefaultState();
    225     private final State mValidatedState = new ValidatedState();
    226     private final State mMaybeNotifyState = new MaybeNotifyState();
    227     private final State mEvaluatingState = new EvaluatingState();
    228     private final State mCaptivePortalState = new CaptivePortalState();
    229 
    230     private CustomIntentReceiver mLaunchCaptivePortalAppBroadcastReceiver = null;
    231 
    232     private final LocalLog validationLogs = new LocalLog(20); // 20 lines
    233 
    234     private final Stopwatch mEvaluationTimer = new Stopwatch();
    235 
    236     // This variable is set before transitioning to the mCaptivePortalState.
    237     private CaptivePortalProbeResult mLastPortalProbeResult = CaptivePortalProbeResult.FAILED;
    238 
    239     public NetworkMonitor(Context context, Handler handler, NetworkAgentInfo networkAgentInfo,
    240             NetworkRequest defaultRequest) {
    241         this(context, handler, networkAgentInfo, defaultRequest, new IpConnectivityLog());
    242     }
    243 
    244     @VisibleForTesting
    245     protected NetworkMonitor(Context context, Handler handler, NetworkAgentInfo networkAgentInfo,
    246             NetworkRequest defaultRequest, IpConnectivityLog logger) {
    247         // Add suffix indicating which NetworkMonitor we're talking about.
    248         super(TAG + networkAgentInfo.name());
    249 
    250         mContext = context;
    251         mMetricsLog = logger;
    252         mConnectivityServiceHandler = handler;
    253         mNetworkAgentInfo = networkAgentInfo;
    254         mNetId = mNetworkAgentInfo.network.netId;
    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         mDefaultRequest = defaultRequest;
    259 
    260         addState(mDefaultState);
    261         addState(mValidatedState, mDefaultState);
    262         addState(mMaybeNotifyState, mDefaultState);
    263             addState(mEvaluatingState, mMaybeNotifyState);
    264             addState(mCaptivePortalState, mMaybeNotifyState);
    265         setInitialState(mDefaultState);
    266 
    267         mIsCaptivePortalCheckEnabled = Settings.Global.getInt(mContext.getContentResolver(),
    268                 Settings.Global.CAPTIVE_PORTAL_DETECTION_ENABLED, 1) == 1;
    269         mUseHttps = Settings.Global.getInt(mContext.getContentResolver(),
    270                 Settings.Global.CAPTIVE_PORTAL_USE_HTTPS, 1) == 1;
    271 
    272         start();
    273     }
    274 
    275     @Override
    276     protected void log(String s) {
    277         if (DBG) Log.d(TAG + "/" + mNetworkAgentInfo.name(), s);
    278     }
    279 
    280     private void validationLog(String s) {
    281         if (DBG) log(s);
    282         validationLogs.log(s);
    283     }
    284 
    285     public ReadOnlyLocalLog getValidationLogs() {
    286         return validationLogs.readOnlyLocalLog();
    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             switch (message.what) {
    295                 case CMD_NETWORK_CONNECTED:
    296                     logNetworkEvent(NetworkEvent.NETWORK_CONNECTED);
    297                     transitionTo(mEvaluatingState);
    298                     return HANDLED;
    299                 case CMD_NETWORK_DISCONNECTED:
    300                     logNetworkEvent(NetworkEvent.NETWORK_DISCONNECTED);
    301                     if (mLaunchCaptivePortalAppBroadcastReceiver != null) {
    302                         mContext.unregisterReceiver(mLaunchCaptivePortalAppBroadcastReceiver);
    303                         mLaunchCaptivePortalAppBroadcastReceiver = null;
    304                     }
    305                     quit();
    306                     return HANDLED;
    307                 case CMD_FORCE_REEVALUATION:
    308                 case CMD_CAPTIVE_PORTAL_RECHECK:
    309                     log("Forcing reevaluation for UID " + message.arg1);
    310                     mUidResponsibleForReeval = message.arg1;
    311                     transitionTo(mEvaluatingState);
    312                     return HANDLED;
    313                 case CMD_CAPTIVE_PORTAL_APP_FINISHED:
    314                     log("CaptivePortal App responded with " + message.arg1);
    315 
    316                     // If the user has seen and acted on a captive portal notification, and the
    317                     // captive portal app is now closed, disable HTTPS probes. This avoids the
    318                     // following pathological situation:
    319                     //
    320                     // 1. HTTP probe returns a captive portal, HTTPS probe fails or times out.
    321                     // 2. User opens the app and logs into the captive portal.
    322                     // 3. HTTP starts working, but HTTPS still doesn't work for some other reason -
    323                     //    perhaps due to the network blocking HTTPS?
    324                     //
    325                     // In this case, we'll fail to validate the network even after the app is
    326                     // dismissed. There is now no way to use this network, because the app is now
    327                     // gone, so the user cannot select "Use this network as is".
    328                     mUseHttps = false;
    329 
    330                     switch (message.arg1) {
    331                         case APP_RETURN_DISMISSED:
    332                             sendMessage(CMD_FORCE_REEVALUATION, 0 /* no UID */, 0);
    333                             break;
    334                         case APP_RETURN_WANTED_AS_IS:
    335                             mDontDisplaySigninNotification = true;
    336                             // TODO: Distinguish this from a network that actually validates.
    337                             // Displaying the "!" on the system UI icon may still be a good idea.
    338                             transitionTo(mValidatedState);
    339                             break;
    340                         case APP_RETURN_UNWANTED:
    341                             mDontDisplaySigninNotification = true;
    342                             mUserDoesNotWant = true;
    343                             mConnectivityServiceHandler.sendMessage(obtainMessage(
    344                                     EVENT_NETWORK_TESTED, NETWORK_TEST_RESULT_INVALID,
    345                                     mNetId, null));
    346                             // TODO: Should teardown network.
    347                             mUidResponsibleForReeval = 0;
    348                             transitionTo(mEvaluatingState);
    349                             break;
    350                     }
    351                     return HANDLED;
    352                 default:
    353                     return HANDLED;
    354             }
    355         }
    356     }
    357 
    358     // Being in the ValidatedState State indicates a Network is:
    359     // - Successfully validated, or
    360     // - Wanted "as is" by the user, or
    361     // - Does not satisfy the default NetworkRequest and so validation has been skipped.
    362     private class ValidatedState extends State {
    363         @Override
    364         public void enter() {
    365             maybeLogEvaluationResult(NetworkEvent.NETWORK_VALIDATED);
    366             mConnectivityServiceHandler.sendMessage(obtainMessage(EVENT_NETWORK_TESTED,
    367                     NETWORK_TEST_RESULT_VALID, mNetworkAgentInfo.network.netId, null));
    368         }
    369 
    370         @Override
    371         public boolean processMessage(Message message) {
    372             switch (message.what) {
    373                 case CMD_NETWORK_CONNECTED:
    374                     transitionTo(mValidatedState);
    375                     return HANDLED;
    376                 default:
    377                     return NOT_HANDLED;
    378             }
    379         }
    380     }
    381 
    382     // Being in the MaybeNotifyState State indicates the user may have been notified that sign-in
    383     // is required.  This State takes care to clear the notification upon exit from the State.
    384     private class MaybeNotifyState extends State {
    385         @Override
    386         public boolean processMessage(Message message) {
    387             switch (message.what) {
    388                 case CMD_LAUNCH_CAPTIVE_PORTAL_APP:
    389                     final Intent intent = new Intent(
    390                             ConnectivityManager.ACTION_CAPTIVE_PORTAL_SIGN_IN);
    391                     intent.putExtra(ConnectivityManager.EXTRA_NETWORK, mNetworkAgentInfo.network);
    392                     intent.putExtra(ConnectivityManager.EXTRA_CAPTIVE_PORTAL,
    393                             new CaptivePortal(new ICaptivePortal.Stub() {
    394                                 @Override
    395                                 public void appResponse(int response) {
    396                                     if (response == APP_RETURN_WANTED_AS_IS) {
    397                                         mContext.enforceCallingPermission(
    398                                                 android.Manifest.permission.CONNECTIVITY_INTERNAL,
    399                                                 "CaptivePortal");
    400                                     }
    401                                     sendMessage(CMD_CAPTIVE_PORTAL_APP_FINISHED, response);
    402                                 }
    403                             }));
    404                     intent.putExtra(ConnectivityManager.EXTRA_CAPTIVE_PORTAL_URL,
    405                             mLastPortalProbeResult.detectUrl);
    406                     intent.setFlags(
    407                             Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT | Intent.FLAG_ACTIVITY_NEW_TASK);
    408                     mContext.startActivityAsUser(intent, UserHandle.CURRENT);
    409                     return HANDLED;
    410                 default:
    411                     return NOT_HANDLED;
    412             }
    413         }
    414 
    415         @Override
    416         public void exit() {
    417             Message message = obtainMessage(EVENT_PROVISIONING_NOTIFICATION, 0,
    418                     mNetworkAgentInfo.network.netId, null);
    419             mConnectivityServiceHandler.sendMessage(message);
    420         }
    421     }
    422 
    423     /**
    424      * Result of calling isCaptivePortal().
    425      * @hide
    426      */
    427     @VisibleForTesting
    428     public static final class CaptivePortalProbeResult {
    429         static final CaptivePortalProbeResult FAILED = new CaptivePortalProbeResult(599);
    430 
    431         private final int mHttpResponseCode;  // HTTP response code returned from Internet probe.
    432         final String redirectUrl;             // Redirect destination returned from Internet probe.
    433         final String detectUrl;               // URL where a 204 response code indicates
    434                                               // captive portal has been appeased.
    435 
    436         public CaptivePortalProbeResult(
    437                 int httpResponseCode, String redirectUrl, String detectUrl) {
    438             mHttpResponseCode = httpResponseCode;
    439             this.redirectUrl = redirectUrl;
    440             this.detectUrl = detectUrl;
    441         }
    442 
    443         public CaptivePortalProbeResult(int httpResponseCode) {
    444             this(httpResponseCode, null, null);
    445         }
    446 
    447         boolean isSuccessful() { return mHttpResponseCode == 204; }
    448         boolean isPortal() {
    449             return !isSuccessful() && mHttpResponseCode >= 200 && mHttpResponseCode <= 399;
    450         }
    451     }
    452 
    453     // Being in the EvaluatingState State indicates the Network is being evaluated for internet
    454     // connectivity, or that the user has indicated that this network is unwanted.
    455     private class EvaluatingState extends State {
    456         private int mReevaluateDelayMs;
    457         private int mAttempts;
    458 
    459         @Override
    460         public void enter() {
    461             // If we have already started to track time spent in EvaluatingState
    462             // don't reset the timer due simply to, say, commands or events that
    463             // cause us to exit and re-enter EvaluatingState.
    464             if (!mEvaluationTimer.isStarted()) {
    465                 mEvaluationTimer.start();
    466             }
    467             sendMessage(CMD_REEVALUATE, ++mReevaluateToken, 0);
    468             if (mUidResponsibleForReeval != INVALID_UID) {
    469                 TrafficStats.setThreadStatsUid(mUidResponsibleForReeval);
    470                 mUidResponsibleForReeval = INVALID_UID;
    471             }
    472             mReevaluateDelayMs = INITIAL_REEVALUATE_DELAY_MS;
    473             mAttempts = 0;
    474         }
    475 
    476         @Override
    477         public boolean processMessage(Message message) {
    478             switch (message.what) {
    479                 case CMD_REEVALUATE:
    480                     if (message.arg1 != mReevaluateToken || mUserDoesNotWant)
    481                         return HANDLED;
    482                     // Don't bother validating networks that don't satisify the default request.
    483                     // This includes:
    484                     //  - VPNs which can be considered explicitly desired by the user and the
    485                     //    user's desire trumps whether the network validates.
    486                     //  - Networks that don't provide internet access.  It's unclear how to
    487                     //    validate such networks.
    488                     //  - Untrusted networks.  It's unsafe to prompt the user to sign-in to
    489                     //    such networks and the user didn't express interest in connecting to
    490                     //    such networks (an app did) so the user may be unhappily surprised when
    491                     //    asked to sign-in to a network they didn't want to connect to in the
    492                     //    first place.  Validation could be done to adjust the network scores
    493                     //    however these networks are app-requested and may not be intended for
    494                     //    general usage, in which case general validation may not be an accurate
    495                     //    measure of the network's quality.  Only the app knows how to evaluate
    496                     //    the network so don't bother validating here.  Furthermore sending HTTP
    497                     //    packets over the network may be undesirable, for example an extremely
    498                     //    expensive metered network, or unwanted leaking of the User Agent string.
    499                     if (!mDefaultRequest.networkCapabilities.satisfiedByNetworkCapabilities(
    500                             mNetworkAgentInfo.networkCapabilities)) {
    501                         validationLog("Network would not satisfy default request, not validating");
    502                         transitionTo(mValidatedState);
    503                         return HANDLED;
    504                     }
    505                     mAttempts++;
    506                     // Note: This call to isCaptivePortal() could take up to a minute. Resolving the
    507                     // server's IP addresses could hit the DNS timeout, and attempting connections
    508                     // to each of the server's several IP addresses (currently one IPv4 and one
    509                     // IPv6) could each take SOCKET_TIMEOUT_MS.  During this time this StateMachine
    510                     // will be unresponsive. isCaptivePortal() could be executed on another Thread
    511                     // if this is found to cause problems.
    512                     CaptivePortalProbeResult probeResult = isCaptivePortal();
    513                     if (probeResult.isSuccessful()) {
    514                         transitionTo(mValidatedState);
    515                     } else if (probeResult.isPortal()) {
    516                         mConnectivityServiceHandler.sendMessage(obtainMessage(EVENT_NETWORK_TESTED,
    517                                 NETWORK_TEST_RESULT_INVALID, mNetId, probeResult.redirectUrl));
    518                         mLastPortalProbeResult = probeResult;
    519                         transitionTo(mCaptivePortalState);
    520                     } else {
    521                         final Message msg = obtainMessage(CMD_REEVALUATE, ++mReevaluateToken, 0);
    522                         sendMessageDelayed(msg, mReevaluateDelayMs);
    523                         logNetworkEvent(NetworkEvent.NETWORK_VALIDATION_FAILED);
    524                         mConnectivityServiceHandler.sendMessage(obtainMessage(
    525                                 EVENT_NETWORK_TESTED, NETWORK_TEST_RESULT_INVALID, mNetId,
    526                                 probeResult.redirectUrl));
    527                         if (mAttempts >= BLAME_FOR_EVALUATION_ATTEMPTS) {
    528                             // Don't continue to blame UID forever.
    529                             TrafficStats.clearThreadStatsUid();
    530                         }
    531                         mReevaluateDelayMs *= 2;
    532                         if (mReevaluateDelayMs > MAX_REEVALUATE_DELAY_MS) {
    533                             mReevaluateDelayMs = MAX_REEVALUATE_DELAY_MS;
    534                         }
    535                     }
    536                     return HANDLED;
    537                 case CMD_FORCE_REEVALUATION:
    538                     // Before IGNORE_REEVALUATE_ATTEMPTS attempts are made,
    539                     // ignore any re-evaluation requests. After, restart the
    540                     // evaluation process via EvaluatingState#enter.
    541                     return (mAttempts < IGNORE_REEVALUATE_ATTEMPTS) ? HANDLED : NOT_HANDLED;
    542                 default:
    543                     return NOT_HANDLED;
    544             }
    545         }
    546 
    547         @Override
    548         public void exit() {
    549             TrafficStats.clearThreadStatsUid();
    550         }
    551     }
    552 
    553     // BroadcastReceiver that waits for a particular Intent and then posts a message.
    554     private class CustomIntentReceiver extends BroadcastReceiver {
    555         private final int mToken;
    556         private final int mWhat;
    557         private final String mAction;
    558         CustomIntentReceiver(String action, int token, int what) {
    559             mToken = token;
    560             mWhat = what;
    561             mAction = action + "_" + mNetworkAgentInfo.network.netId + "_" + token;
    562             mContext.registerReceiver(this, new IntentFilter(mAction));
    563         }
    564         public PendingIntent getPendingIntent() {
    565             final Intent intent = new Intent(mAction);
    566             intent.setPackage(mContext.getPackageName());
    567             return PendingIntent.getBroadcast(mContext, 0, intent, 0);
    568         }
    569         @Override
    570         public void onReceive(Context context, Intent intent) {
    571             if (intent.getAction().equals(mAction)) sendMessage(obtainMessage(mWhat, mToken));
    572         }
    573     }
    574 
    575     // Being in the CaptivePortalState State indicates a captive portal was detected and the user
    576     // has been shown a notification to sign-in.
    577     private class CaptivePortalState extends State {
    578         private static final String ACTION_LAUNCH_CAPTIVE_PORTAL_APP =
    579                 "android.net.netmon.launchCaptivePortalApp";
    580 
    581         @Override
    582         public void enter() {
    583             maybeLogEvaluationResult(NetworkEvent.NETWORK_CAPTIVE_PORTAL_FOUND);
    584             // Don't annoy user with sign-in notifications.
    585             if (mDontDisplaySigninNotification) return;
    586             // Create a CustomIntentReceiver that sends us a
    587             // CMD_LAUNCH_CAPTIVE_PORTAL_APP message when the user
    588             // touches the notification.
    589             if (mLaunchCaptivePortalAppBroadcastReceiver == null) {
    590                 // Wait for result.
    591                 mLaunchCaptivePortalAppBroadcastReceiver = new CustomIntentReceiver(
    592                         ACTION_LAUNCH_CAPTIVE_PORTAL_APP, new Random().nextInt(),
    593                         CMD_LAUNCH_CAPTIVE_PORTAL_APP);
    594             }
    595             // Display the sign in notification.
    596             Message message = obtainMessage(EVENT_PROVISIONING_NOTIFICATION, 1,
    597                     mNetworkAgentInfo.network.netId,
    598                     mLaunchCaptivePortalAppBroadcastReceiver.getPendingIntent());
    599             mConnectivityServiceHandler.sendMessage(message);
    600             // Retest for captive portal occasionally.
    601             sendMessageDelayed(CMD_CAPTIVE_PORTAL_RECHECK, 0 /* no UID */,
    602                     CAPTIVE_PORTAL_REEVALUATE_DELAY_MS);
    603         }
    604 
    605         @Override
    606         public void exit() {
    607             removeMessages(CMD_CAPTIVE_PORTAL_RECHECK);
    608         }
    609     }
    610 
    611     private static String getCaptivePortalServerHttpsUrl(Context context) {
    612         return getSetting(context, Settings.Global.CAPTIVE_PORTAL_HTTPS_URL, DEFAULT_HTTPS_URL);
    613     }
    614 
    615     public static String getCaptivePortalServerHttpUrl(Context context) {
    616         return getSetting(context, Settings.Global.CAPTIVE_PORTAL_HTTP_URL, DEFAULT_HTTP_URL);
    617     }
    618 
    619     private static String getCaptivePortalFallbackUrl(Context context) {
    620         return getSetting(context,
    621                 Settings.Global.CAPTIVE_PORTAL_FALLBACK_URL, DEFAULT_FALLBACK_URL);
    622     }
    623 
    624     private static String getCaptivePortalUserAgent(Context context) {
    625         return getSetting(context, Settings.Global.CAPTIVE_PORTAL_USER_AGENT, DEFAULT_USER_AGENT);
    626     }
    627 
    628     private static String getSetting(Context context, String symbol, String defaultValue) {
    629         final String value = Settings.Global.getString(context.getContentResolver(), symbol);
    630         return value != null ? value : defaultValue;
    631     }
    632 
    633     @VisibleForTesting
    634     protected CaptivePortalProbeResult isCaptivePortal() {
    635         if (!mIsCaptivePortalCheckEnabled) return new CaptivePortalProbeResult(204);
    636 
    637         URL pacUrl = null, httpsUrl = null, httpUrl = null, fallbackUrl = null;
    638 
    639         // On networks with a PAC instead of fetching a URL that should result in a 204
    640         // response, we instead simply fetch the PAC script.  This is done for a few reasons:
    641         // 1. At present our PAC code does not yet handle multiple PACs on multiple networks
    642         //    until something like https://android-review.googlesource.com/#/c/115180/ lands.
    643         //    Network.openConnection() will ignore network-specific PACs and instead fetch
    644         //    using NO_PROXY.  If a PAC is in place, the only fetch we know will succeed with
    645         //    NO_PROXY is the fetch of the PAC itself.
    646         // 2. To proxy the generate_204 fetch through a PAC would require a number of things
    647         //    happen before the fetch can commence, namely:
    648         //        a) the PAC script be fetched
    649         //        b) a PAC script resolver service be fired up and resolve the captive portal
    650         //           server.
    651         //    Network validation could be delayed until these prerequisities are satisifed or
    652         //    could simply be left to race them.  Neither is an optimal solution.
    653         // 3. PAC scripts are sometimes used to block or restrict Internet access and may in
    654         //    fact block fetching of the generate_204 URL which would lead to false negative
    655         //    results for network validation.
    656         final ProxyInfo proxyInfo = mNetworkAgentInfo.linkProperties.getHttpProxy();
    657         if (proxyInfo != null && !Uri.EMPTY.equals(proxyInfo.getPacFileUrl())) {
    658             pacUrl = makeURL(proxyInfo.getPacFileUrl().toString());
    659             if (pacUrl == null) {
    660                 return CaptivePortalProbeResult.FAILED;
    661             }
    662         }
    663 
    664         if (pacUrl == null) {
    665             httpsUrl = makeURL(getCaptivePortalServerHttpsUrl(mContext));
    666             httpUrl = makeURL(getCaptivePortalServerHttpUrl(mContext));
    667             fallbackUrl = makeURL(getCaptivePortalFallbackUrl(mContext));
    668             if (httpUrl == null || httpsUrl == null) {
    669                 return CaptivePortalProbeResult.FAILED;
    670             }
    671         }
    672 
    673         long startTime = SystemClock.elapsedRealtime();
    674 
    675         // Pre-resolve the captive portal server host so we can log it.
    676         // Only do this if HttpURLConnection is about to, to avoid any potentially
    677         // unnecessary resolution.
    678         String hostToResolve = null;
    679         if (pacUrl != null) {
    680             hostToResolve = pacUrl.getHost();
    681         } else if (proxyInfo != null) {
    682             hostToResolve = proxyInfo.getHost();
    683         } else {
    684             hostToResolve = httpUrl.getHost();
    685         }
    686 
    687         if (!TextUtils.isEmpty(hostToResolve)) {
    688             String probeName = ValidationProbeEvent.getProbeName(ValidationProbeEvent.PROBE_DNS);
    689             final Stopwatch dnsTimer = new Stopwatch().start();
    690             int dnsResult;
    691             long dnsLatency;
    692             try {
    693                 InetAddress[] addresses = mNetworkAgentInfo.network.getAllByName(hostToResolve);
    694                 dnsResult = ValidationProbeEvent.DNS_SUCCESS;
    695                 dnsLatency = dnsTimer.stop();
    696                 final StringBuffer connectInfo = new StringBuffer(", " + hostToResolve + "=");
    697                 for (InetAddress address : addresses) {
    698                     connectInfo.append(address.getHostAddress());
    699                     if (address != addresses[addresses.length-1]) connectInfo.append(",");
    700                 }
    701                 validationLog(probeName + " OK " + dnsLatency + "ms" + connectInfo);
    702             } catch (UnknownHostException e) {
    703                 dnsResult = ValidationProbeEvent.DNS_FAILURE;
    704                 dnsLatency = dnsTimer.stop();
    705                 validationLog(probeName + " FAIL " + dnsLatency + "ms, " + hostToResolve);
    706             }
    707             logValidationProbe(dnsLatency, ValidationProbeEvent.PROBE_DNS, dnsResult);
    708         }
    709 
    710         CaptivePortalProbeResult result;
    711         if (pacUrl != null) {
    712             result = sendHttpProbe(pacUrl, ValidationProbeEvent.PROBE_PAC);
    713         } else if (mUseHttps) {
    714             result = sendParallelHttpProbes(httpsUrl, httpUrl, fallbackUrl);
    715         } else {
    716             result = sendHttpProbe(httpUrl, ValidationProbeEvent.PROBE_HTTP);
    717         }
    718 
    719         long endTime = SystemClock.elapsedRealtime();
    720 
    721         sendNetworkConditionsBroadcast(true /* response received */,
    722                 result.isPortal() /* isCaptivePortal */,
    723                 startTime, endTime);
    724 
    725         return result;
    726     }
    727 
    728     /**
    729      * Do a URL fetch on a known server to see if we get the data we expect.
    730      * Returns HTTP response code.
    731      */
    732     @VisibleForTesting
    733     protected CaptivePortalProbeResult sendHttpProbe(URL url, int probeType) {
    734         HttpURLConnection urlConnection = null;
    735         int httpResponseCode = 599;
    736         String redirectUrl = null;
    737         final Stopwatch probeTimer = new Stopwatch().start();
    738         try {
    739             urlConnection = (HttpURLConnection) mNetworkAgentInfo.network.openConnection(url);
    740             urlConnection.setInstanceFollowRedirects(probeType == ValidationProbeEvent.PROBE_PAC);
    741             urlConnection.setConnectTimeout(SOCKET_TIMEOUT_MS);
    742             urlConnection.setReadTimeout(SOCKET_TIMEOUT_MS);
    743             urlConnection.setUseCaches(false);
    744             final String userAgent = getCaptivePortalUserAgent(mContext);
    745             if (userAgent != null) {
    746                urlConnection.setRequestProperty("User-Agent", userAgent);
    747             }
    748 
    749             // Time how long it takes to get a response to our request
    750             long requestTimestamp = SystemClock.elapsedRealtime();
    751 
    752             httpResponseCode = urlConnection.getResponseCode();
    753             redirectUrl = urlConnection.getHeaderField("location");
    754 
    755             // Time how long it takes to get a response to our request
    756             long responseTimestamp = SystemClock.elapsedRealtime();
    757 
    758             validationLog(ValidationProbeEvent.getProbeName(probeType) + " " + url +
    759                     " time=" + (responseTimestamp - requestTimestamp) + "ms" +
    760                     " ret=" + httpResponseCode +
    761                     " headers=" + urlConnection.getHeaderFields());
    762             // NOTE: We may want to consider an "HTTP/1.0 204" response to be a captive
    763             // portal.  The only example of this seen so far was a captive portal.  For
    764             // the time being go with prior behavior of assuming it's not a captive
    765             // portal.  If it is considered a captive portal, a different sign-in URL
    766             // is needed (i.e. can't browse a 204).  This could be the result of an HTTP
    767             // proxy server.
    768 
    769             // Consider 200 response with "Content-length=0" to not be a captive portal.
    770             // There's no point in considering this a captive portal as the user cannot
    771             // sign-in to an empty page.  Probably the result of a broken transparent proxy.
    772             // See http://b/9972012.
    773             if (httpResponseCode == 200 && urlConnection.getContentLength() == 0) {
    774                 validationLog("Empty 200 response interpreted as 204 response.");
    775                 httpResponseCode = 204;
    776             }
    777 
    778             if (httpResponseCode == 200 && probeType == ValidationProbeEvent.PROBE_PAC) {
    779                 validationLog("PAC fetch 200 response interpreted as 204 response.");
    780                 httpResponseCode = 204;
    781             }
    782         } catch (IOException e) {
    783             validationLog("Probably not a portal: exception " + e);
    784             if (httpResponseCode == 599) {
    785                 // TODO: Ping gateway and DNS server and log results.
    786             }
    787         } finally {
    788             if (urlConnection != null) {
    789                 urlConnection.disconnect();
    790             }
    791         }
    792         logValidationProbe(probeTimer.stop(), probeType, httpResponseCode);
    793         return new CaptivePortalProbeResult(httpResponseCode, redirectUrl, url.toString());
    794     }
    795 
    796     private CaptivePortalProbeResult sendParallelHttpProbes(
    797             URL httpsUrl, URL httpUrl, URL fallbackUrl) {
    798         // Number of probes to wait for. If a probe completes with a conclusive answer
    799         // it shortcuts the latch immediately by forcing the count to 0.
    800         final CountDownLatch latch = new CountDownLatch(2);
    801 
    802         final class ProbeThread extends Thread {
    803             private final boolean mIsHttps;
    804             private volatile CaptivePortalProbeResult mResult = CaptivePortalProbeResult.FAILED;
    805 
    806             public ProbeThread(boolean isHttps) {
    807                 mIsHttps = isHttps;
    808             }
    809 
    810             public CaptivePortalProbeResult result() {
    811                 return mResult;
    812             }
    813 
    814             @Override
    815             public void run() {
    816                 if (mIsHttps) {
    817                     mResult = sendHttpProbe(httpsUrl, ValidationProbeEvent.PROBE_HTTPS);
    818                 } else {
    819                     mResult = sendHttpProbe(httpUrl, ValidationProbeEvent.PROBE_HTTP);
    820                 }
    821                 if ((mIsHttps && mResult.isSuccessful()) || (!mIsHttps && mResult.isPortal())) {
    822                     // Stop waiting immediately if https succeeds or if http finds a portal.
    823                     while (latch.getCount() > 0) {
    824                         latch.countDown();
    825                     }
    826                 }
    827                 // Signal this probe has completed.
    828                 latch.countDown();
    829             }
    830         }
    831 
    832         final ProbeThread httpsProbe = new ProbeThread(true);
    833         final ProbeThread httpProbe = new ProbeThread(false);
    834 
    835         try {
    836             httpsProbe.start();
    837             httpProbe.start();
    838             latch.await(PROBE_TIMEOUT_MS, TimeUnit.MILLISECONDS);
    839         } catch (InterruptedException e) {
    840             validationLog("Error: probes wait interrupted!");
    841             return CaptivePortalProbeResult.FAILED;
    842         }
    843 
    844         final CaptivePortalProbeResult httpsResult = httpsProbe.result();
    845         final CaptivePortalProbeResult httpResult = httpProbe.result();
    846 
    847         // Look for a conclusive probe result first.
    848         if (httpResult.isPortal()) {
    849             return httpResult;
    850         }
    851         // httpsResult.isPortal() is not expected, but check it nonetheless.
    852         if (httpsResult.isPortal() || httpsResult.isSuccessful()) {
    853             return httpsResult;
    854         }
    855         // If a fallback url is specified, use a fallback probe to try again portal detection.
    856         if (fallbackUrl != null) {
    857             CaptivePortalProbeResult result =
    858                     sendHttpProbe(fallbackUrl, ValidationProbeEvent.PROBE_FALLBACK);
    859             if (result.isPortal()) {
    860                 return result;
    861             }
    862         }
    863         // Otherwise wait until https probe completes and use its result.
    864         try {
    865             httpsProbe.join();
    866         } catch (InterruptedException e) {
    867             validationLog("Error: https probe wait interrupted!");
    868             return CaptivePortalProbeResult.FAILED;
    869         }
    870         return httpsProbe.result();
    871     }
    872 
    873     private URL makeURL(String url) {
    874         if (url != null) {
    875             try {
    876                 return new URL(url);
    877             } catch (MalformedURLException e) {
    878                 validationLog("Bad URL: " + url);
    879             }
    880         }
    881         return null;
    882     }
    883 
    884     /**
    885      * @param responseReceived - whether or not we received a valid HTTP response to our request.
    886      * If false, isCaptivePortal and responseTimestampMs are ignored
    887      * TODO: This should be moved to the transports.  The latency could be passed to the transports
    888      * along with the captive portal result.  Currently the TYPE_MOBILE broadcasts appear unused so
    889      * perhaps this could just be added to the WiFi transport only.
    890      */
    891     private void sendNetworkConditionsBroadcast(boolean responseReceived, boolean isCaptivePortal,
    892             long requestTimestampMs, long responseTimestampMs) {
    893         if (Settings.Global.getInt(mContext.getContentResolver(),
    894                 Settings.Global.WIFI_SCAN_ALWAYS_AVAILABLE, 0) == 0) {
    895             return;
    896         }
    897 
    898         if (systemReady == false) return;
    899 
    900         Intent latencyBroadcast = new Intent(ACTION_NETWORK_CONDITIONS_MEASURED);
    901         switch (mNetworkAgentInfo.networkInfo.getType()) {
    902             case ConnectivityManager.TYPE_WIFI:
    903                 WifiInfo currentWifiInfo = mWifiManager.getConnectionInfo();
    904                 if (currentWifiInfo != null) {
    905                     // NOTE: getSSID()'s behavior changed in API 17; before that, SSIDs were not
    906                     // surrounded by double quotation marks (thus violating the Javadoc), but this
    907                     // was changed to match the Javadoc in API 17. Since clients may have started
    908                     // sanitizing the output of this method since API 17 was released, we should
    909                     // not change it here as it would become impossible to tell whether the SSID is
    910                     // simply being surrounded by quotes due to the API, or whether those quotes
    911                     // are actually part of the SSID.
    912                     latencyBroadcast.putExtra(EXTRA_SSID, currentWifiInfo.getSSID());
    913                     latencyBroadcast.putExtra(EXTRA_BSSID, currentWifiInfo.getBSSID());
    914                 } else {
    915                     if (DBG) logw("network info is TYPE_WIFI but no ConnectionInfo found");
    916                     return;
    917                 }
    918                 break;
    919             case ConnectivityManager.TYPE_MOBILE:
    920                 latencyBroadcast.putExtra(EXTRA_NETWORK_TYPE, mTelephonyManager.getNetworkType());
    921                 List<CellInfo> info = mTelephonyManager.getAllCellInfo();
    922                 if (info == null) return;
    923                 int numRegisteredCellInfo = 0;
    924                 for (CellInfo cellInfo : info) {
    925                     if (cellInfo.isRegistered()) {
    926                         numRegisteredCellInfo++;
    927                         if (numRegisteredCellInfo > 1) {
    928                             log("more than one registered CellInfo.  Can't " +
    929                                     "tell which is active.  Bailing.");
    930                             return;
    931                         }
    932                         if (cellInfo instanceof CellInfoCdma) {
    933                             CellIdentityCdma cellId = ((CellInfoCdma) cellInfo).getCellIdentity();
    934                             latencyBroadcast.putExtra(EXTRA_CELL_ID, cellId);
    935                         } else if (cellInfo instanceof CellInfoGsm) {
    936                             CellIdentityGsm cellId = ((CellInfoGsm) cellInfo).getCellIdentity();
    937                             latencyBroadcast.putExtra(EXTRA_CELL_ID, cellId);
    938                         } else if (cellInfo instanceof CellInfoLte) {
    939                             CellIdentityLte cellId = ((CellInfoLte) cellInfo).getCellIdentity();
    940                             latencyBroadcast.putExtra(EXTRA_CELL_ID, cellId);
    941                         } else if (cellInfo instanceof CellInfoWcdma) {
    942                             CellIdentityWcdma cellId = ((CellInfoWcdma) cellInfo).getCellIdentity();
    943                             latencyBroadcast.putExtra(EXTRA_CELL_ID, cellId);
    944                         } else {
    945                             if (DBG) logw("Registered cellinfo is unrecognized");
    946                             return;
    947                         }
    948                     }
    949                 }
    950                 break;
    951             default:
    952                 return;
    953         }
    954         latencyBroadcast.putExtra(EXTRA_CONNECTIVITY_TYPE, mNetworkAgentInfo.networkInfo.getType());
    955         latencyBroadcast.putExtra(EXTRA_RESPONSE_RECEIVED, responseReceived);
    956         latencyBroadcast.putExtra(EXTRA_REQUEST_TIMESTAMP_MS, requestTimestampMs);
    957 
    958         if (responseReceived) {
    959             latencyBroadcast.putExtra(EXTRA_IS_CAPTIVE_PORTAL, isCaptivePortal);
    960             latencyBroadcast.putExtra(EXTRA_RESPONSE_TIMESTAMP_MS, responseTimestampMs);
    961         }
    962         mContext.sendBroadcastAsUser(latencyBroadcast, UserHandle.CURRENT,
    963                 PERMISSION_ACCESS_NETWORK_CONDITIONS);
    964     }
    965 
    966     private void logNetworkEvent(int evtype) {
    967         mMetricsLog.log(new NetworkEvent(mNetId, evtype));
    968     }
    969 
    970     private void maybeLogEvaluationResult(int evtype) {
    971         if (mEvaluationTimer.isRunning()) {
    972             mMetricsLog.log(new NetworkEvent(mNetId, evtype, mEvaluationTimer.stop()));
    973             mEvaluationTimer.reset();
    974         }
    975     }
    976 
    977     private void logValidationProbe(long durationMs, int probeType, int probeResult) {
    978         mMetricsLog.log(new ValidationProbeEvent(mNetId, durationMs, probeType, probeResult));
    979     }
    980 }
    981