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;
EvaluationResult(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;
ValidationStage(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 
NetworkMonitor(Context context, Handler handler, NetworkAgentInfo networkAgentInfo, NetworkRequest defaultRequest)274     public NetworkMonitor(Context context, Handler handler, NetworkAgentInfo networkAgentInfo,
275             NetworkRequest defaultRequest) {
276         this(context, handler, networkAgentInfo, defaultRequest, new IpConnectivityLog());
277     }
278 
279     @VisibleForTesting
NetworkMonitor(Context context, Handler handler, NetworkAgentInfo networkAgentInfo, NetworkRequest defaultRequest, IpConnectivityLog logger)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
log(String s)317     protected void log(String s) {
318         if (DBG) Log.d(TAG + "/" + mNetworkAgentInfo.name(), s);
319     }
320 
validationLog(int probeType, Object url, String msg)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 
validationLog(String s)326     private void validationLog(String s) {
327         if (DBG) log(s);
328         validationLogs.log(s);
329     }
330 
getValidationLogs()331     public ReadOnlyLocalLog getValidationLogs() {
332         return validationLogs.readOnlyLocalLog();
333     }
334 
validationStage()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
processMessage(Message message)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
enter()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
processMessage(Message message)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
processMessage(Message message)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
exit()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 
CaptivePortalProbeResult( int httpResponseCode, String redirectUrl, String detectUrl)490         public CaptivePortalProbeResult(
491                 int httpResponseCode, String redirectUrl, String detectUrl) {
492             mHttpResponseCode = httpResponseCode;
493             this.redirectUrl = redirectUrl;
494             this.detectUrl = detectUrl;
495         }
496 
CaptivePortalProbeResult(int httpResponseCode)497         public CaptivePortalProbeResult(int httpResponseCode) {
498             this(httpResponseCode, null, null);
499         }
500 
isSuccessful()501         boolean isSuccessful() { return mHttpResponseCode == 204; }
isPortal()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
enter()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
processMessage(Message message)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
exit()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;
CustomIntentReceiver(String action, int token, int what)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         }
getPendingIntent()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
onReceive(Context context, Intent intent)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
enter()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
exit()662         public void exit() {
663             removeMessages(CMD_CAPTIVE_PORTAL_RECHECK);
664         }
665     }
666 
getCaptivePortalServerHttpsUrl(Context context)667     private static String getCaptivePortalServerHttpsUrl(Context context) {
668         return getSetting(context, Settings.Global.CAPTIVE_PORTAL_HTTPS_URL, DEFAULT_HTTPS_URL);
669     }
670 
getCaptivePortalServerHttpUrl(Context context)671     public static String getCaptivePortalServerHttpUrl(Context context) {
672         return getSetting(context, Settings.Global.CAPTIVE_PORTAL_HTTP_URL, DEFAULT_HTTP_URL);
673     }
674 
makeCaptivePortalFallbackUrls(Context context)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 
getCaptivePortalUserAgent(Context context)695     private static String getCaptivePortalUserAgent(Context context) {
696         return getSetting(context, Settings.Global.CAPTIVE_PORTAL_USER_AGENT, DEFAULT_USER_AGENT);
697     }
698 
getSetting(Context context, String symbol, String defaultValue)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 
nextFallbackUrl()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
isCaptivePortal()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      */
sendDnsAndHttpProbes(ProxyInfo proxy, URL url, int probeType)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. */
sendDnsProbe(String host)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
sendHttpProbe(URL url, int probeType)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 
sendParallelHttpProbes( ProxyInfo proxy, URL httpsUrl, URL httpUrl)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 
makeURL(String url)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      */
sendNetworkConditionsBroadcast(boolean responseReceived, boolean isCaptivePortal, long requestTimestampMs, long responseTimestampMs)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 
logNetworkEvent(int evtype)1070     private void logNetworkEvent(int evtype) {
1071         mMetricsLog.log(new NetworkEvent(mNetId, evtype));
1072     }
1073 
networkEventType(ValidationStage s, EvaluationResult r)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 
maybeLogEvaluationResult(int evtype)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 
logValidationProbe(long durationMs, int probeType, int probeResult)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