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