CaptivePortalTracker.java revision 9d4204d6257bf69885cf0718aa3adaa17457c318
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.content.BroadcastReceiver; 20import android.content.Context; 21import android.content.Intent; 22import android.content.IntentFilter; 23import android.database.ContentObserver; 24import android.net.ConnectivityManager; 25import android.net.IConnectivityManager; 26import android.net.wifi.WifiInfo; 27import android.net.wifi.WifiManager; 28import android.os.Handler; 29import android.os.Message; 30import android.os.RemoteException; 31import android.os.SystemClock; 32import android.provider.Settings; 33import android.telephony.CellIdentityCdma; 34import android.telephony.CellIdentityGsm; 35import android.telephony.CellIdentityLte; 36import android.telephony.CellIdentityWcdma; 37import android.telephony.CellInfo; 38import android.telephony.CellInfoCdma; 39import android.telephony.CellInfoGsm; 40import android.telephony.CellInfoLte; 41import android.telephony.CellInfoWcdma; 42import android.telephony.TelephonyManager; 43 44import com.android.internal.util.State; 45import com.android.internal.util.StateMachine; 46 47import java.io.IOException; 48import java.net.HttpURLConnection; 49import java.net.InetAddress; 50import java.net.Inet4Address; 51import java.net.SocketTimeoutException; 52import java.net.URL; 53import java.net.UnknownHostException; 54import java.util.List; 55 56/** 57 * This class allows captive portal detection on a network. 58 * @hide 59 */ 60public class CaptivePortalTracker extends StateMachine { 61 private static final boolean DBG = true; 62 private static final String TAG = "CaptivePortalTracker"; 63 64 private static final String DEFAULT_SERVER = "clients3.google.com"; 65 66 private static final int SOCKET_TIMEOUT_MS = 10000; 67 68 public static final String ACTION_NETWORK_CONDITIONS_MEASURED = 69 "android.net.conn.NETWORK_CONDITIONS_MEASURED"; 70 public static final String EXTRA_CONNECTIVITY_TYPE = "extra_connectivity_type"; 71 public static final String EXTRA_NETWORK_TYPE = "extra_network_type"; 72 public static final String EXTRA_RESPONSE_RECEIVED = "extra_response_received"; 73 public static final String EXTRA_IS_CAPTIVE_PORTAL = "extra_is_captive_portal"; 74 public static final String EXTRA_CELL_ID = "extra_cellid"; 75 public static final String EXTRA_SSID = "extra_ssid"; 76 public static final String EXTRA_BSSID = "extra_bssid"; 77 /** real time since boot */ 78 public static final String EXTRA_REQUEST_TIMESTAMP_MS = "extra_request_timestamp_ms"; 79 public static final String EXTRA_RESPONSE_TIMESTAMP_MS = "extra_response_timestamp_ms"; 80 81 private static final String PERMISSION_ACCESS_NETWORK_CONDITIONS = 82 "android.permission.ACCESS_NETWORK_CONDITIONS"; 83 84 private String mServer; 85 private String mUrl; 86 private boolean mIsCaptivePortalCheckEnabled = false; 87 private IConnectivityManager mConnService; 88 private TelephonyManager mTelephonyManager; 89 private WifiManager mWifiManager; 90 private Context mContext; 91 private NetworkInfo mNetworkInfo; 92 93 private static final int CMD_DETECT_PORTAL = 0; 94 private static final int CMD_CONNECTIVITY_CHANGE = 1; 95 private static final int CMD_DELAYED_CAPTIVE_CHECK = 2; 96 97 /* This delay happens every time before we do a captive check on a network */ 98 private static final int DELAYED_CHECK_INTERVAL_MS = 10000; 99 private int mDelayedCheckToken = 0; 100 101 private State mDefaultState = new DefaultState(); 102 private State mNoActiveNetworkState = new NoActiveNetworkState(); 103 private State mActiveNetworkState = new ActiveNetworkState(); 104 private State mDelayedCaptiveCheckState = new DelayedCaptiveCheckState(); 105 106 private static final String SETUP_WIZARD_PACKAGE = "com.google.android.setupwizard"; 107 private boolean mDeviceProvisioned = false; 108 private ProvisioningObserver mProvisioningObserver; 109 110 private CaptivePortalTracker(Context context, IConnectivityManager cs) { 111 super(TAG); 112 113 mContext = context; 114 mConnService = cs; 115 mTelephonyManager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE); 116 mWifiManager = (WifiManager) context.getSystemService(Context.WIFI_SERVICE); 117 mProvisioningObserver = new ProvisioningObserver(); 118 119 IntentFilter filter = new IntentFilter(); 120 filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION); 121 filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION_IMMEDIATE); 122 mContext.registerReceiver(mReceiver, filter); 123 124 mServer = Settings.Global.getString(mContext.getContentResolver(), 125 Settings.Global.CAPTIVE_PORTAL_SERVER); 126 if (mServer == null) mServer = DEFAULT_SERVER; 127 128 mIsCaptivePortalCheckEnabled = Settings.Global.getInt(mContext.getContentResolver(), 129 Settings.Global.CAPTIVE_PORTAL_DETECTION_ENABLED, 1) == 1; 130 131 addState(mDefaultState); 132 addState(mNoActiveNetworkState, mDefaultState); 133 addState(mActiveNetworkState, mDefaultState); 134 addState(mDelayedCaptiveCheckState, mActiveNetworkState); 135 setInitialState(mNoActiveNetworkState); 136 } 137 138 private class ProvisioningObserver extends ContentObserver { 139 ProvisioningObserver() { 140 super(new Handler()); 141 mContext.getContentResolver().registerContentObserver(Settings.Global.getUriFor( 142 Settings.Global.DEVICE_PROVISIONED), false, this); 143 onChange(false); // load initial value 144 } 145 146 @Override 147 public void onChange(boolean selfChange) { 148 mDeviceProvisioned = Settings.Global.getInt(mContext.getContentResolver(), 149 Settings.Global.DEVICE_PROVISIONED, 0) != 0; 150 } 151 } 152 153 private final BroadcastReceiver mReceiver = new BroadcastReceiver() { 154 @Override 155 public void onReceive(Context context, Intent intent) { 156 String action = intent.getAction(); 157 // Normally, we respond to CONNECTIVITY_ACTION, allowing time for the change in 158 // connectivity to stabilize, but if the device is not yet provisioned, respond 159 // immediately to speed up transit through the setup wizard. 160 if ((mDeviceProvisioned && action.equals(ConnectivityManager.CONNECTIVITY_ACTION)) 161 || (!mDeviceProvisioned 162 && action.equals(ConnectivityManager.CONNECTIVITY_ACTION_IMMEDIATE))) { 163 NetworkInfo info = intent.getParcelableExtra( 164 ConnectivityManager.EXTRA_NETWORK_INFO); 165 sendMessage(obtainMessage(CMD_CONNECTIVITY_CHANGE, info)); 166 } 167 } 168 }; 169 170 public static CaptivePortalTracker makeCaptivePortalTracker(Context context, 171 IConnectivityManager cs) { 172 CaptivePortalTracker captivePortal = new CaptivePortalTracker(context, cs); 173 captivePortal.start(); 174 return captivePortal; 175 } 176 177 public void detectCaptivePortal(NetworkInfo info) { 178 sendMessage(obtainMessage(CMD_DETECT_PORTAL, info)); 179 } 180 181 private class DefaultState extends State { 182 @Override 183 public void enter() { 184 setNotificationOff(); 185 } 186 187 @Override 188 public boolean processMessage(Message message) { 189 if (DBG) log(getName() + message.toString()); 190 switch (message.what) { 191 case CMD_DETECT_PORTAL: 192 NetworkInfo info = (NetworkInfo) message.obj; 193 // Checking on a secondary connection is not supported 194 // yet 195 notifyPortalCheckComplete(info); 196 break; 197 case CMD_CONNECTIVITY_CHANGE: 198 case CMD_DELAYED_CAPTIVE_CHECK: 199 break; 200 default: 201 loge("Ignoring " + message); 202 break; 203 } 204 return HANDLED; 205 } 206 } 207 208 private class NoActiveNetworkState extends State { 209 @Override 210 public void enter() { 211 mNetworkInfo = null; 212 } 213 214 @Override 215 public boolean processMessage(Message message) { 216 if (DBG) log(getName() + message.toString()); 217 InetAddress server; 218 NetworkInfo info; 219 switch (message.what) { 220 case CMD_CONNECTIVITY_CHANGE: 221 info = (NetworkInfo) message.obj; 222 if (info.getType() == ConnectivityManager.TYPE_WIFI) { 223 if (info.isConnected() && isActiveNetwork(info)) { 224 mNetworkInfo = info; 225 transitionTo(mDelayedCaptiveCheckState); 226 } 227 } else { 228 log(getName() + " not a wifi connectivity change, ignore"); 229 } 230 break; 231 default: 232 return NOT_HANDLED; 233 } 234 return HANDLED; 235 } 236 } 237 238 private class ActiveNetworkState extends State { 239 @Override 240 public void enter() { 241 setNotificationOff(); 242 } 243 244 @Override 245 public boolean processMessage(Message message) { 246 NetworkInfo info; 247 switch (message.what) { 248 case CMD_CONNECTIVITY_CHANGE: 249 info = (NetworkInfo) message.obj; 250 if (!info.isConnected() 251 && info.getType() == mNetworkInfo.getType()) { 252 if (DBG) log("Disconnected from active network " + info); 253 transitionTo(mNoActiveNetworkState); 254 } else if (info.getType() != mNetworkInfo.getType() && 255 info.isConnected() && 256 isActiveNetwork(info)) { 257 if (DBG) log("Active network switched " + info); 258 deferMessage(message); 259 transitionTo(mNoActiveNetworkState); 260 } 261 break; 262 default: 263 return NOT_HANDLED; 264 } 265 return HANDLED; 266 } 267 } 268 269 270 271 private class DelayedCaptiveCheckState extends State { 272 @Override 273 public void enter() { 274 Message message = obtainMessage(CMD_DELAYED_CAPTIVE_CHECK, ++mDelayedCheckToken, 0); 275 if (mDeviceProvisioned) { 276 sendMessageDelayed(message, DELAYED_CHECK_INTERVAL_MS); 277 } else { 278 sendMessage(message); 279 } 280 } 281 282 @Override 283 public boolean processMessage(Message message) { 284 if (DBG) log(getName() + message.toString()); 285 switch (message.what) { 286 case CMD_DELAYED_CAPTIVE_CHECK: 287 if (message.arg1 == mDelayedCheckToken) { 288 InetAddress server = lookupHost(mServer); 289 boolean captive = server != null && isCaptivePortal(server); 290 if (captive) { 291 if (DBG) log("Captive network " + mNetworkInfo); 292 } else { 293 if (DBG) log("Not captive network " + mNetworkInfo); 294 } 295 notifyPortalCheckCompleted(mNetworkInfo, captive); 296 if (mDeviceProvisioned) { 297 if (captive) { 298 // Setup Wizard will assist the user in connecting to a captive 299 // portal, so make the notification visible unless during setup 300 try { 301 mConnService.setProvisioningNotificationVisible(true, 302 mNetworkInfo.getType(), mNetworkInfo.getExtraInfo(), mUrl); 303 } catch(RemoteException e) { 304 e.printStackTrace(); 305 } 306 } 307 } else { 308 Intent intent = new Intent( 309 ConnectivityManager.ACTION_CAPTIVE_PORTAL_TEST_COMPLETED); 310 intent.putExtra(ConnectivityManager.EXTRA_IS_CAPTIVE_PORTAL, captive); 311 intent.setPackage(SETUP_WIZARD_PACKAGE); 312 mContext.sendBroadcast(intent); 313 } 314 315 transitionTo(mActiveNetworkState); 316 } 317 break; 318 default: 319 return NOT_HANDLED; 320 } 321 return HANDLED; 322 } 323 } 324 325 private void notifyPortalCheckComplete(NetworkInfo info) { 326 if (info == null) { 327 loge("notifyPortalCheckComplete on null"); 328 return; 329 } 330 try { 331 if (DBG) log("notifyPortalCheckComplete: ni=" + info); 332 mConnService.captivePortalCheckComplete(info); 333 } catch(RemoteException e) { 334 e.printStackTrace(); 335 } 336 } 337 338 private void notifyPortalCheckCompleted(NetworkInfo info, boolean isCaptivePortal) { 339 if (info == null) { 340 loge("notifyPortalCheckComplete on null"); 341 return; 342 } 343 try { 344 if (DBG) log("notifyPortalCheckCompleted: captive=" + isCaptivePortal + " ni=" + info); 345 mConnService.captivePortalCheckCompleted(info, isCaptivePortal); 346 } catch(RemoteException e) { 347 e.printStackTrace(); 348 } 349 } 350 351 private boolean isActiveNetwork(NetworkInfo info) { 352 try { 353 NetworkInfo active = mConnService.getActiveNetworkInfo(); 354 if (active != null && active.getType() == info.getType()) { 355 return true; 356 } 357 } catch (RemoteException e) { 358 e.printStackTrace(); 359 } 360 return false; 361 } 362 363 private void setNotificationOff() { 364 try { 365 mConnService.setProvisioningNotificationVisible(false, ConnectivityManager.TYPE_NONE, 366 null, null); 367 } catch (RemoteException e) { 368 log("setNotificationOff: " + e); 369 } 370 } 371 372 /** 373 * Do a URL fetch on a known server to see if we get the data we expect. 374 * Measure the response time and broadcast that. 375 */ 376 private boolean isCaptivePortal(InetAddress server) { 377 HttpURLConnection urlConnection = null; 378 if (!mIsCaptivePortalCheckEnabled) return false; 379 380 mUrl = "http://" + server.getHostAddress() + "/generate_204"; 381 if (DBG) log("Checking " + mUrl); 382 long requestTimestamp = -1; 383 try { 384 URL url = new URL(mUrl); 385 urlConnection = (HttpURLConnection) url.openConnection(); 386 urlConnection.setInstanceFollowRedirects(false); 387 urlConnection.setConnectTimeout(SOCKET_TIMEOUT_MS); 388 urlConnection.setReadTimeout(SOCKET_TIMEOUT_MS); 389 urlConnection.setUseCaches(false); 390 391 // Time how long it takes to get a response to our request 392 requestTimestamp = SystemClock.elapsedRealtime(); 393 394 urlConnection.getInputStream(); 395 396 // Time how long it takes to get a response to our request 397 long responseTimestamp = SystemClock.elapsedRealtime(); 398 399 // we got a valid response, but not from the real google 400 int rspCode = urlConnection.getResponseCode(); 401 boolean isCaptivePortal = rspCode != 204; 402 403 sendNetworkConditionsBroadcast(true /* response received */, isCaptivePortal, 404 requestTimestamp, responseTimestamp); 405 406 if (DBG) log("isCaptivePortal: ret=" + isCaptivePortal + " rspCode=" + rspCode); 407 return isCaptivePortal; 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 sendFailedCaptivePortalCheckBroadcast(long requestTimestampMs) { 439 sendNetworkConditionsBroadcast(false /* response received */, false /* ignored */, 440 requestTimestampMs, 0 /* ignored */); 441 } 442 443 /** 444 * @param responseReceived - whether or not we received a valid HTTP response to our request. 445 * If false, isCaptivePortal and responseTimestampMs are ignored 446 */ 447 private void sendNetworkConditionsBroadcast(boolean responseReceived, boolean isCaptivePortal, 448 long requestTimestampMs, long responseTimestampMs) { 449 if (Settings.Global.getInt(mContext.getContentResolver(), 450 Settings.Global.WIFI_SCAN_ALWAYS_AVAILABLE, 0) == 0) { 451 if (DBG) log("Don't send network conditions - lacking user consent."); 452 return; 453 } 454 455 Intent latencyBroadcast = new Intent(ACTION_NETWORK_CONDITIONS_MEASURED); 456 switch (mNetworkInfo.getType()) { 457 case ConnectivityManager.TYPE_WIFI: 458 WifiInfo currentWifiInfo = mWifiManager.getConnectionInfo(); 459 if (currentWifiInfo != null) { 460 latencyBroadcast.putExtra(EXTRA_SSID, currentWifiInfo.getSSID()); 461 latencyBroadcast.putExtra(EXTRA_BSSID, currentWifiInfo.getBSSID()); 462 } else { 463 if (DBG) logw("network info is TYPE_WIFI but no ConnectionInfo found"); 464 return; 465 } 466 break; 467 case ConnectivityManager.TYPE_MOBILE: 468 latencyBroadcast.putExtra(EXTRA_NETWORK_TYPE, mTelephonyManager.getNetworkType()); 469 List<CellInfo> info = mTelephonyManager.getAllCellInfo(); 470 if (info == null) return; 471 StringBuffer uniqueCellId = new StringBuffer(); 472 int numRegisteredCellInfo = 0; 473 for (CellInfo cellInfo : info) { 474 if (cellInfo.isRegistered()) { 475 numRegisteredCellInfo++; 476 if (numRegisteredCellInfo > 1) { 477 if (DBG) log("more than one registered CellInfo. Can't " + 478 "tell which is active. Bailing."); 479 return; 480 } 481 if (cellInfo instanceof CellInfoCdma) { 482 CellIdentityCdma cellId = ((CellInfoCdma) cellInfo).getCellIdentity(); 483 latencyBroadcast.putExtra(EXTRA_CELL_ID, cellId); 484 } else if (cellInfo instanceof CellInfoGsm) { 485 CellIdentityGsm cellId = ((CellInfoGsm) cellInfo).getCellIdentity(); 486 latencyBroadcast.putExtra(EXTRA_CELL_ID, cellId); 487 } else if (cellInfo instanceof CellInfoLte) { 488 CellIdentityLte cellId = ((CellInfoLte) cellInfo).getCellIdentity(); 489 latencyBroadcast.putExtra(EXTRA_CELL_ID, cellId); 490 } else if (cellInfo instanceof CellInfoWcdma) { 491 CellIdentityWcdma cellId = ((CellInfoWcdma) cellInfo).getCellIdentity(); 492 latencyBroadcast.putExtra(EXTRA_CELL_ID, cellId); 493 } else { 494 if (DBG) logw("Registered cellinfo is unrecognized"); 495 return; 496 } 497 } 498 } 499 break; 500 default: 501 return; 502 } 503 latencyBroadcast.putExtra(EXTRA_CONNECTIVITY_TYPE, mNetworkInfo.getType()); 504 latencyBroadcast.putExtra(EXTRA_RESPONSE_RECEIVED, responseReceived); 505 latencyBroadcast.putExtra(EXTRA_REQUEST_TIMESTAMP_MS, requestTimestampMs); 506 507 if (responseReceived) { 508 latencyBroadcast.putExtra(EXTRA_IS_CAPTIVE_PORTAL, isCaptivePortal); 509 latencyBroadcast.putExtra(EXTRA_RESPONSE_TIMESTAMP_MS, responseTimestampMs); 510 } 511 mContext.sendBroadcast(latencyBroadcast, PERMISSION_ACCESS_NETWORK_CONDITIONS); 512 } 513} 514