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