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