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