CaptivePortalTracker.java revision 89d16f7597d9e03bf3cf9eb1ba91b590ab1ac892
1/* 2 * Copyright (C) 2012 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 android.net; 18 19import android.app.Activity; 20import android.app.Notification; 21import android.app.NotificationManager; 22import android.app.PendingIntent; 23import android.content.BroadcastReceiver; 24import android.content.Context; 25import android.content.Intent; 26import android.content.IntentFilter; 27import android.content.res.Resources; 28import android.database.ContentObserver; 29import android.net.ConnectivityManager; 30import android.net.IConnectivityManager; 31import android.net.wifi.WifiInfo; 32import android.net.wifi.WifiManager; 33import android.os.Handler; 34import android.os.UserHandle; 35import android.os.Message; 36import android.os.RemoteException; 37import android.os.SystemClock; 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; 49import android.text.TextUtils; 50 51import com.android.internal.util.State; 52import com.android.internal.util.StateMachine; 53 54import java.io.IOException; 55import java.net.HttpURLConnection; 56import java.net.InetAddress; 57import java.net.Inet4Address; 58import java.net.SocketTimeoutException; 59import java.net.URL; 60import java.net.UnknownHostException; 61import java.util.List; 62 63import com.android.internal.R; 64 65/** 66 * This class allows captive portal detection on a network. 67 * @hide 68 */ 69public class CaptivePortalTracker extends StateMachine { 70 private static final boolean DBG = true; 71 private static final String TAG = "CaptivePortalTracker"; 72 73 private static final String DEFAULT_SERVER = "clients3.google.com"; 74 private static final String NOTIFICATION_ID = "CaptivePortal.Notification"; 75 76 private static final int SOCKET_TIMEOUT_MS = 10000; 77 78 public static final String ACTION_NETWORK_CONDITIONS_MEASURED = 79 "android.net.conn.NETWORK_CONDITIONS_MEASURED"; 80 public static final String EXTRA_CONNECTIVITY_TYPE = "extra_connectivity_type"; 81 public static final String EXTRA_NETWORK_TYPE = "extra_network_type"; 82 public static final String EXTRA_RESPONSE_RECEIVED = "extra_response_received"; 83 public static final String EXTRA_IS_CAPTIVE_PORTAL = "extra_is_captive_portal"; 84 public static final String EXTRA_CELL_ID = "extra_cellid"; 85 public static final String EXTRA_SSID = "extra_ssid"; 86 public static final String EXTRA_BSSID = "extra_bssid"; 87 /** real time since boot */ 88 public static final String EXTRA_REQUEST_TIMESTAMP_MS = "extra_request_timestamp_ms"; 89 public static final String EXTRA_RESPONSE_TIMESTAMP_MS = "extra_response_timestamp_ms"; 90 91 private static final String PERMISSION_ACCESS_NETWORK_CONDITIONS = 92 "android.permission.ACCESS_NETWORK_CONDITIONS"; 93 94 private String mServer; 95 private String mUrl; 96 private boolean mNotificationShown = false; 97 private boolean mIsCaptivePortalCheckEnabled = false; 98 private IConnectivityManager mConnService; 99 private TelephonyManager mTelephonyManager; 100 private WifiManager mWifiManager; 101 private Context mContext; 102 private NetworkInfo mNetworkInfo; 103 104 private static final int CMD_DETECT_PORTAL = 0; 105 private static final int CMD_CONNECTIVITY_CHANGE = 1; 106 private static final int CMD_DELAYED_CAPTIVE_CHECK = 2; 107 108 /* This delay happens every time before we do a captive check on a network */ 109 private static final int DELAYED_CHECK_INTERVAL_MS = 10000; 110 private int mDelayedCheckToken = 0; 111 112 private State mDefaultState = new DefaultState(); 113 private State mNoActiveNetworkState = new NoActiveNetworkState(); 114 private State mActiveNetworkState = new ActiveNetworkState(); 115 private State mDelayedCaptiveCheckState = new DelayedCaptiveCheckState(); 116 117 private static final String SETUP_WIZARD_PACKAGE = "com.google.android.setupwizard"; 118 private boolean mDeviceProvisioned = false; 119 private ProvisioningObserver mProvisioningObserver; 120 121 private CaptivePortalTracker(Context context, IConnectivityManager cs) { 122 super(TAG); 123 124 mContext = context; 125 mConnService = cs; 126 mTelephonyManager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE); 127 mWifiManager = (WifiManager) context.getSystemService(Context.WIFI_SERVICE); 128 mProvisioningObserver = new ProvisioningObserver(); 129 130 IntentFilter filter = new IntentFilter(); 131 filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION); 132 filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION_IMMEDIATE); 133 mContext.registerReceiver(mReceiver, filter); 134 135 mServer = Settings.Global.getString(mContext.getContentResolver(), 136 Settings.Global.CAPTIVE_PORTAL_SERVER); 137 if (mServer == null) mServer = DEFAULT_SERVER; 138 139 mIsCaptivePortalCheckEnabled = Settings.Global.getInt(mContext.getContentResolver(), 140 Settings.Global.CAPTIVE_PORTAL_DETECTION_ENABLED, 1) == 1; 141 142 addState(mDefaultState); 143 addState(mNoActiveNetworkState, mDefaultState); 144 addState(mActiveNetworkState, mDefaultState); 145 addState(mDelayedCaptiveCheckState, mActiveNetworkState); 146 setInitialState(mNoActiveNetworkState); 147 } 148 149 private class ProvisioningObserver extends ContentObserver { 150 ProvisioningObserver() { 151 super(new Handler()); 152 mContext.getContentResolver().registerContentObserver(Settings.Global.getUriFor( 153 Settings.Global.DEVICE_PROVISIONED), false, this); 154 onChange(false); // load initial value 155 } 156 157 @Override 158 public void onChange(boolean selfChange) { 159 mDeviceProvisioned = Settings.Global.getInt(mContext.getContentResolver(), 160 Settings.Global.DEVICE_PROVISIONED, 0) != 0; 161 } 162 } 163 164 private final BroadcastReceiver mReceiver = new BroadcastReceiver() { 165 @Override 166 public void onReceive(Context context, Intent intent) { 167 String action = intent.getAction(); 168 // Normally, we respond to CONNECTIVITY_ACTION, allowing time for the change in 169 // connectivity to stabilize, but if the device is not yet provisioned, respond 170 // immediately to speed up transit through the setup wizard. 171 if ((mDeviceProvisioned && action.equals(ConnectivityManager.CONNECTIVITY_ACTION)) 172 || (!mDeviceProvisioned 173 && action.equals(ConnectivityManager.CONNECTIVITY_ACTION_IMMEDIATE))) { 174 NetworkInfo info = intent.getParcelableExtra( 175 ConnectivityManager.EXTRA_NETWORK_INFO); 176 sendMessage(obtainMessage(CMD_CONNECTIVITY_CHANGE, info)); 177 } 178 } 179 }; 180 181 public static CaptivePortalTracker makeCaptivePortalTracker(Context context, 182 IConnectivityManager cs) { 183 CaptivePortalTracker captivePortal = new CaptivePortalTracker(context, cs); 184 captivePortal.start(); 185 return captivePortal; 186 } 187 188 public void detectCaptivePortal(NetworkInfo info) { 189 sendMessage(obtainMessage(CMD_DETECT_PORTAL, info)); 190 } 191 192 private class DefaultState extends State { 193 @Override 194 public void enter() { 195 if (DBG) log(getName() + "\n"); 196 } 197 198 @Override 199 public boolean processMessage(Message message) { 200 if (DBG) log(getName() + message.toString() + "\n"); 201 switch (message.what) { 202 case CMD_DETECT_PORTAL: 203 NetworkInfo info = (NetworkInfo) message.obj; 204 // Checking on a secondary connection is not supported 205 // yet 206 notifyPortalCheckComplete(info); 207 break; 208 case CMD_CONNECTIVITY_CHANGE: 209 case CMD_DELAYED_CAPTIVE_CHECK: 210 break; 211 default: 212 loge("Ignoring " + message); 213 break; 214 } 215 return HANDLED; 216 } 217 } 218 219 private class NoActiveNetworkState extends State { 220 @Override 221 public void enter() { 222 if (DBG) log(getName() + "\n"); 223 mNetworkInfo = null; 224 /* Clear any previous notification */ 225 setNotificationVisible(false); 226 } 227 228 @Override 229 public boolean processMessage(Message message) { 230 if (DBG) log(getName() + message.toString() + "\n"); 231 InetAddress server; 232 NetworkInfo info; 233 switch (message.what) { 234 case CMD_CONNECTIVITY_CHANGE: 235 info = (NetworkInfo) message.obj; 236 if (info.isConnected() && isActiveNetwork(info)) { 237 mNetworkInfo = info; 238 transitionTo(mDelayedCaptiveCheckState); 239 } 240 break; 241 default: 242 return NOT_HANDLED; 243 } 244 return HANDLED; 245 } 246 } 247 248 private class ActiveNetworkState extends State { 249 @Override 250 public void enter() { 251 if (DBG) log(getName() + "\n"); 252 } 253 254 @Override 255 public boolean processMessage(Message message) { 256 NetworkInfo info; 257 switch (message.what) { 258 case CMD_CONNECTIVITY_CHANGE: 259 info = (NetworkInfo) message.obj; 260 if (!info.isConnected() 261 && info.getType() == mNetworkInfo.getType()) { 262 if (DBG) log("Disconnected from active network " + info); 263 transitionTo(mNoActiveNetworkState); 264 } else if (info.getType() != mNetworkInfo.getType() && 265 info.isConnected() && 266 isActiveNetwork(info)) { 267 if (DBG) log("Active network switched " + info); 268 deferMessage(message); 269 transitionTo(mNoActiveNetworkState); 270 } 271 break; 272 default: 273 return NOT_HANDLED; 274 } 275 return HANDLED; 276 } 277 } 278 279 280 281 private class DelayedCaptiveCheckState extends State { 282 @Override 283 public void enter() { 284 if (DBG) log(getName() + "\n"); 285 Message message = obtainMessage(CMD_DELAYED_CAPTIVE_CHECK, ++mDelayedCheckToken, 0); 286 if (mDeviceProvisioned) { 287 sendMessageDelayed(message, DELAYED_CHECK_INTERVAL_MS); 288 } else { 289 sendMessage(message); 290 } 291 } 292 293 @Override 294 public boolean processMessage(Message message) { 295 if (DBG) log(getName() + message.toString() + "\n"); 296 switch (message.what) { 297 case CMD_DELAYED_CAPTIVE_CHECK: 298 if (message.arg1 == mDelayedCheckToken) { 299 InetAddress server = lookupHost(mServer); 300 boolean captive = server != null && isCaptivePortal(server); 301 if (captive) { 302 if (DBG) log("Captive network " + mNetworkInfo); 303 } else { 304 if (DBG) log("Not captive network " + mNetworkInfo); 305 } 306 notifyPortalCheckCompleted(mNetworkInfo, captive); 307 if (mDeviceProvisioned) { 308 if (captive) { 309 // Setup Wizard will assist the user in connecting to a captive 310 // portal, so make the notification visible unless during setup 311 setNotificationVisible(true); 312 } 313 } else { 314 Intent intent = new Intent( 315 ConnectivityManager.ACTION_CAPTIVE_PORTAL_TEST_COMPLETED); 316 intent.putExtra(ConnectivityManager.EXTRA_IS_CAPTIVE_PORTAL, captive); 317 intent.setPackage(SETUP_WIZARD_PACKAGE); 318 mContext.sendBroadcast(intent); 319 } 320 321 transitionTo(mActiveNetworkState); 322 } 323 break; 324 default: 325 return NOT_HANDLED; 326 } 327 return HANDLED; 328 } 329 } 330 331 private void notifyPortalCheckComplete(NetworkInfo info) { 332 if (info == null) { 333 loge("notifyPortalCheckComplete on null"); 334 return; 335 } 336 try { 337 if (DBG) log("notifyPortalCheckComplete: ni=" + info); 338 mConnService.captivePortalCheckComplete(info); 339 } catch(RemoteException e) { 340 e.printStackTrace(); 341 } 342 } 343 344 private void notifyPortalCheckCompleted(NetworkInfo info, boolean isCaptivePortal) { 345 if (info == null) { 346 loge("notifyPortalCheckComplete on null"); 347 return; 348 } 349 try { 350 if (DBG) log("notifyPortalCheckCompleted: captive=" + isCaptivePortal + " ni=" + info); 351 mConnService.captivePortalCheckCompleted(info, isCaptivePortal); 352 } catch(RemoteException e) { 353 e.printStackTrace(); 354 } 355 } 356 357 private boolean isActiveNetwork(NetworkInfo info) { 358 try { 359 NetworkInfo active = mConnService.getActiveNetworkInfo(); 360 if (active != null && active.getType() == info.getType()) { 361 return true; 362 } 363 } catch (RemoteException e) { 364 e.printStackTrace(); 365 } 366 return false; 367 } 368 369 /** 370 * Do a URL fetch on a known server to see if we get the data we expect. 371 * Measure the response time and broadcast that. 372 */ 373 private boolean isCaptivePortal(InetAddress server) { 374 HttpURLConnection urlConnection = null; 375 if (!mIsCaptivePortalCheckEnabled) return false; 376 377 mUrl = "http://" + server.getHostAddress() + "/generate_204"; 378 if (DBG) log("Checking " + mUrl); 379 long requestTimestamp = -1; 380 try { 381 URL url = new URL(mUrl); 382 urlConnection = (HttpURLConnection) url.openConnection(); 383 urlConnection.setInstanceFollowRedirects(false); 384 urlConnection.setConnectTimeout(SOCKET_TIMEOUT_MS); 385 urlConnection.setReadTimeout(SOCKET_TIMEOUT_MS); 386 urlConnection.setUseCaches(false); 387 388 // Time how long it takes to get a response to our request 389 requestTimestamp = SystemClock.elapsedRealtime(); 390 391 urlConnection.getInputStream(); 392 393 // Time how long it takes to get a response to our request 394 long responseTimestamp = SystemClock.elapsedRealtime(); 395 396 // we got a valid response, but not from the real google 397 boolean isCaptivePortal = urlConnection.getResponseCode() != 204; 398 399 sendNetworkConditionsBroadcast(true /* response received */, isCaptivePortal, 400 requestTimestamp, responseTimestamp); 401 return isCaptivePortal; 402 } catch (SocketTimeoutException e) { 403 if (DBG) log("Probably a portal: exception " + e); 404 if (requestTimestamp != -1) { 405 sendFailedCaptivePortalCheckBroadcast(requestTimestamp); 406 } // else something went wrong with setting up the urlConnection 407 return true; 408 } catch (IOException e) { 409 if (DBG) log("Probably not a portal: exception " + e); 410 if (requestTimestamp != -1) { 411 sendFailedCaptivePortalCheckBroadcast(requestTimestamp); 412 } // else something went wrong with setting up the urlConnection 413 return false; 414 } finally { 415 if (urlConnection != null) { 416 urlConnection.disconnect(); 417 } 418 } 419 } 420 421 private InetAddress lookupHost(String hostname) { 422 InetAddress inetAddress[]; 423 try { 424 inetAddress = InetAddress.getAllByName(hostname); 425 } catch (UnknownHostException e) { 426 sendFailedCaptivePortalCheckBroadcast(SystemClock.elapsedRealtime()); 427 return null; 428 } 429 430 for (InetAddress a : inetAddress) { 431 if (a instanceof Inet4Address) return a; 432 } 433 434 sendFailedCaptivePortalCheckBroadcast(SystemClock.elapsedRealtime()); 435 return null; 436 } 437 438 private void setNotificationVisible(boolean visible) { 439 // if it should be hidden and it is already hidden, then noop 440 if (!visible && !mNotificationShown) { 441 if (DBG) log("setNotivicationVisible: false and not shown, so noop"); 442 return; 443 } 444 445 Resources r = Resources.getSystem(); 446 NotificationManager notificationManager = (NotificationManager) mContext 447 .getSystemService(Context.NOTIFICATION_SERVICE); 448 449 if (visible) { 450 CharSequence title; 451 CharSequence details; 452 int icon; 453 String url = null; 454 switch (mNetworkInfo.getType()) { 455 case ConnectivityManager.TYPE_WIFI: 456 title = r.getString(R.string.wifi_available_sign_in, 0); 457 details = r.getString(R.string.network_available_sign_in_detailed, 458 mNetworkInfo.getExtraInfo()); 459 icon = R.drawable.stat_notify_wifi_in_range; 460 url = mUrl; 461 break; 462 case ConnectivityManager.TYPE_MOBILE: 463 title = r.getString(R.string.network_available_sign_in, 0); 464 // TODO: Change this to pull from NetworkInfo once a printable 465 // name has been added to it 466 details = mTelephonyManager.getNetworkOperatorName(); 467 icon = R.drawable.stat_notify_rssi_in_range; 468 try { 469 url = mConnService.getMobileProvisioningUrl(); 470 if (TextUtils.isEmpty(url)) { 471 url = mConnService.getMobileRedirectedProvisioningUrl(); 472 } 473 } catch(RemoteException e) { 474 e.printStackTrace(); 475 } 476 if (TextUtils.isEmpty(url)) { 477 url = mUrl; 478 } 479 break; 480 default: 481 title = r.getString(R.string.network_available_sign_in, 0); 482 details = r.getString(R.string.network_available_sign_in_detailed, 483 mNetworkInfo.getExtraInfo()); 484 icon = R.drawable.stat_notify_rssi_in_range; 485 url = mUrl; 486 break; 487 } 488 489 Notification notification = new Notification(); 490 notification.when = 0; 491 notification.icon = icon; 492 notification.flags = Notification.FLAG_AUTO_CANCEL; 493 Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); 494 intent.setFlags(Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT | 495 Intent.FLAG_ACTIVITY_NEW_TASK); 496 notification.contentIntent = PendingIntent.getActivity(mContext, 0, intent, 0); 497 notification.tickerText = title; 498 notification.setLatestEventInfo(mContext, title, details, notification.contentIntent); 499 500 if (DBG) log("setNotivicationVisible: make visible"); 501 notificationManager.notify(NOTIFICATION_ID, 1, notification); 502 } else { 503 if (DBG) log("setNotivicationVisible: cancel notification"); 504 notificationManager.cancel(NOTIFICATION_ID, 1); 505 } 506 mNotificationShown = visible; 507 } 508 509 private void sendFailedCaptivePortalCheckBroadcast(long requestTimestampMs) { 510 sendNetworkConditionsBroadcast(false /* response received */, false /* ignored */, 511 requestTimestampMs, 0 /* ignored */); 512 } 513 514 /** 515 * @param responseReceived - whether or not we received a valid HTTP response to our request. 516 * If false, isCaptivePortal and responseTimestampMs are ignored 517 */ 518 private void sendNetworkConditionsBroadcast(boolean responseReceived, boolean isCaptivePortal, 519 long requestTimestampMs, long responseTimestampMs) { 520 if (Settings.Global.getInt(mContext.getContentResolver(), 521 Settings.Global.WIFI_SCAN_ALWAYS_AVAILABLE, 0) == 0) { 522 if (DBG) log("Don't send network conditions - lacking user consent."); 523 return; 524 } 525 526 Intent latencyBroadcast = new Intent(ACTION_NETWORK_CONDITIONS_MEASURED); 527 switch (mNetworkInfo.getType()) { 528 case ConnectivityManager.TYPE_WIFI: 529 WifiInfo currentWifiInfo = mWifiManager.getConnectionInfo(); 530 if (currentWifiInfo != null) { 531 latencyBroadcast.putExtra(EXTRA_SSID, currentWifiInfo.getSSID()); 532 latencyBroadcast.putExtra(EXTRA_BSSID, currentWifiInfo.getBSSID()); 533 } else { 534 if (DBG) logw("network info is TYPE_WIFI but no ConnectionInfo found"); 535 return; 536 } 537 break; 538 case ConnectivityManager.TYPE_MOBILE: 539 latencyBroadcast.putExtra(EXTRA_NETWORK_TYPE, mTelephonyManager.getNetworkType()); 540 List<CellInfo> info = mTelephonyManager.getAllCellInfo(); 541 if (info == null) return; 542 StringBuffer uniqueCellId = new StringBuffer(); 543 int numRegisteredCellInfo = 0; 544 for (CellInfo cellInfo : info) { 545 if (cellInfo.isRegistered()) { 546 numRegisteredCellInfo++; 547 if (numRegisteredCellInfo > 1) { 548 if (DBG) log("more than one registered CellInfo. Can't " + 549 "tell which is active. Bailing."); 550 return; 551 } 552 if (cellInfo instanceof CellInfoCdma) { 553 CellIdentityCdma cellId = ((CellInfoCdma) cellInfo).getCellIdentity(); 554 latencyBroadcast.putExtra(EXTRA_CELL_ID, cellId); 555 } else if (cellInfo instanceof CellInfoGsm) { 556 CellIdentityGsm cellId = ((CellInfoGsm) cellInfo).getCellIdentity(); 557 latencyBroadcast.putExtra(EXTRA_CELL_ID, cellId); 558 } else if (cellInfo instanceof CellInfoLte) { 559 CellIdentityLte cellId = ((CellInfoLte) cellInfo).getCellIdentity(); 560 latencyBroadcast.putExtra(EXTRA_CELL_ID, cellId); 561 } else if (cellInfo instanceof CellInfoWcdma) { 562 CellIdentityWcdma cellId = ((CellInfoWcdma) cellInfo).getCellIdentity(); 563 latencyBroadcast.putExtra(EXTRA_CELL_ID, cellId); 564 } else { 565 if (DBG) logw("Registered cellinfo is unrecognized"); 566 return; 567 } 568 } 569 } 570 break; 571 default: 572 return; 573 } 574 latencyBroadcast.putExtra(EXTRA_CONNECTIVITY_TYPE, mNetworkInfo.getType()); 575 latencyBroadcast.putExtra(EXTRA_RESPONSE_RECEIVED, responseReceived); 576 latencyBroadcast.putExtra(EXTRA_REQUEST_TIMESTAMP_MS, requestTimestampMs); 577 578 if (responseReceived) { 579 latencyBroadcast.putExtra(EXTRA_IS_CAPTIVE_PORTAL, isCaptivePortal); 580 latencyBroadcast.putExtra(EXTRA_RESPONSE_TIMESTAMP_MS, responseTimestampMs); 581 } 582 mContext.sendBroadcast(latencyBroadcast, PERMISSION_ACCESS_NETWORK_CONDITIONS); 583 } 584} 585