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 17 package android.net; 18 19 import android.content.BroadcastReceiver; 20 import android.content.Context; 21 import android.content.Intent; 22 import android.content.IntentFilter; 23 import android.database.ContentObserver; 24 import android.net.ConnectivityManager; 25 import android.net.IConnectivityManager; 26 import android.net.wifi.WifiInfo; 27 import android.net.wifi.WifiManager; 28 import android.os.Handler; 29 import android.os.Message; 30 import android.os.RemoteException; 31 import android.os.SystemClock; 32 import android.provider.Settings; 33 import android.telephony.CellIdentityCdma; 34 import android.telephony.CellIdentityGsm; 35 import android.telephony.CellIdentityLte; 36 import android.telephony.CellIdentityWcdma; 37 import android.telephony.CellInfo; 38 import android.telephony.CellInfoCdma; 39 import android.telephony.CellInfoGsm; 40 import android.telephony.CellInfoLte; 41 import android.telephony.CellInfoWcdma; 42 import android.telephony.TelephonyManager; 43 44 import com.android.internal.util.State; 45 import com.android.internal.util.StateMachine; 46 47 import java.io.IOException; 48 import java.net.HttpURLConnection; 49 import java.net.InetAddress; 50 import java.net.Inet4Address; 51 import java.net.SocketTimeoutException; 52 import java.net.URL; 53 import java.net.UnknownHostException; 54 import java.util.List; 55 56 /** 57 * This class allows captive portal detection on a network. 58 * @hide 59 */ 60 public 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 183 @Override 184 public boolean processMessage(Message message) { 185 if (DBG) log(getName() + message.toString()); 186 switch (message.what) { 187 case CMD_DETECT_PORTAL: 188 NetworkInfo info = (NetworkInfo) message.obj; 189 // Checking on a secondary connection is not supported 190 // yet 191 notifyPortalCheckComplete(info); 192 break; 193 case CMD_CONNECTIVITY_CHANGE: 194 case CMD_DELAYED_CAPTIVE_CHECK: 195 break; 196 default: 197 loge("Ignoring " + message); 198 break; 199 } 200 return HANDLED; 201 } 202 } 203 204 private class NoActiveNetworkState extends State { 205 @Override 206 public void enter() { 207 setNotificationOff(); 208 mNetworkInfo = null; 209 } 210 211 @Override 212 public boolean processMessage(Message message) { 213 if (DBG) log(getName() + message.toString()); 214 InetAddress server; 215 NetworkInfo info; 216 switch (message.what) { 217 case CMD_CONNECTIVITY_CHANGE: 218 info = (NetworkInfo) message.obj; 219 if (info.getType() == ConnectivityManager.TYPE_WIFI) { 220 if (info.isConnected() && isActiveNetwork(info)) { 221 mNetworkInfo = info; 222 transitionTo(mDelayedCaptiveCheckState); 223 } 224 } else { 225 log(getName() + " not a wifi connectivity change, ignore"); 226 } 227 break; 228 default: 229 return NOT_HANDLED; 230 } 231 return HANDLED; 232 } 233 } 234 235 private class ActiveNetworkState extends State { 236 @Override 237 public boolean processMessage(Message message) { 238 NetworkInfo info; 239 switch (message.what) { 240 case CMD_CONNECTIVITY_CHANGE: 241 info = (NetworkInfo) message.obj; 242 if (!info.isConnected() 243 && info.getType() == mNetworkInfo.getType()) { 244 if (DBG) log("Disconnected from active network " + info); 245 transitionTo(mNoActiveNetworkState); 246 } else if (info.getType() != mNetworkInfo.getType() && 247 info.isConnected() && 248 isActiveNetwork(info)) { 249 if (DBG) log("Active network switched " + info); 250 deferMessage(message); 251 transitionTo(mNoActiveNetworkState); 252 } 253 break; 254 default: 255 return NOT_HANDLED; 256 } 257 return HANDLED; 258 } 259 } 260 261 262 263 private class DelayedCaptiveCheckState extends State { 264 @Override 265 public void enter() { 266 Message message = obtainMessage(CMD_DELAYED_CAPTIVE_CHECK, ++mDelayedCheckToken, 0); 267 if (mDeviceProvisioned) { 268 sendMessageDelayed(message, DELAYED_CHECK_INTERVAL_MS); 269 } else { 270 sendMessage(message); 271 } 272 } 273 274 @Override 275 public boolean processMessage(Message message) { 276 if (DBG) log(getName() + message.toString()); 277 switch (message.what) { 278 case CMD_DELAYED_CAPTIVE_CHECK: 279 setNotificationOff(); 280 281 if (message.arg1 == mDelayedCheckToken) { 282 InetAddress server = lookupHost(mServer); 283 boolean captive = server != null && isCaptivePortal(server); 284 if (captive) { 285 if (DBG) log("Captive network " + mNetworkInfo); 286 } else { 287 if (DBG) log("Not captive network " + mNetworkInfo); 288 } 289 notifyPortalCheckCompleted(mNetworkInfo, captive); 290 if (mDeviceProvisioned) { 291 if (captive) { 292 // Setup Wizard will assist the user in connecting to a captive 293 // portal, so make the notification visible unless during setup 294 try { 295 mConnService.setProvisioningNotificationVisible(true, 296 mNetworkInfo.getType(), mNetworkInfo.getExtraInfo(), mUrl); 297 } catch(RemoteException e) { 298 e.printStackTrace(); 299 } 300 } 301 } else { 302 Intent intent = new Intent( 303 ConnectivityManager.ACTION_CAPTIVE_PORTAL_TEST_COMPLETED); 304 intent.putExtra(ConnectivityManager.EXTRA_IS_CAPTIVE_PORTAL, captive); 305 intent.setPackage(SETUP_WIZARD_PACKAGE); 306 mContext.sendBroadcast(intent); 307 } 308 309 transitionTo(mActiveNetworkState); 310 } 311 break; 312 default: 313 return NOT_HANDLED; 314 } 315 return HANDLED; 316 } 317 } 318 319 private void notifyPortalCheckComplete(NetworkInfo info) { 320 if (info == null) { 321 loge("notifyPortalCheckComplete on null"); 322 return; 323 } 324 try { 325 if (DBG) log("notifyPortalCheckComplete: ni=" + info); 326 mConnService.captivePortalCheckComplete(info); 327 } catch(RemoteException e) { 328 e.printStackTrace(); 329 } 330 } 331 332 private void notifyPortalCheckCompleted(NetworkInfo info, boolean isCaptivePortal) { 333 if (info == null) { 334 loge("notifyPortalCheckComplete on null"); 335 return; 336 } 337 try { 338 if (DBG) log("notifyPortalCheckCompleted: captive=" + isCaptivePortal + " ni=" + info); 339 mConnService.captivePortalCheckCompleted(info, isCaptivePortal); 340 } catch(RemoteException e) { 341 e.printStackTrace(); 342 } 343 } 344 345 private boolean isActiveNetwork(NetworkInfo info) { 346 try { 347 NetworkInfo active = mConnService.getActiveNetworkInfo(); 348 if (active != null && active.getType() == info.getType()) { 349 return true; 350 } 351 } catch (RemoteException e) { 352 e.printStackTrace(); 353 } 354 return false; 355 } 356 357 private void setNotificationOff() { 358 try { 359 if (mNetworkInfo != null) { 360 mConnService.setProvisioningNotificationVisible(false, mNetworkInfo.getType(), 361 null, null); 362 } 363 } catch (RemoteException e) { 364 log("setNotificationOff: " + e); 365 } 366 } 367 368 /** 369 * Do a URL fetch on a known server to see if we get the data we expect. 370 * Measure the response time and broadcast that. 371 */ 372 private boolean isCaptivePortal(InetAddress server) { 373 HttpURLConnection urlConnection = null; 374 if (!mIsCaptivePortalCheckEnabled) return false; 375 376 mUrl = "http://" + server.getHostAddress() + "/generate_204"; 377 if (DBG) log("Checking " + mUrl); 378 long requestTimestamp = -1; 379 try { 380 URL url = new URL(mUrl); 381 urlConnection = (HttpURLConnection) url.openConnection(); 382 urlConnection.setInstanceFollowRedirects(false); 383 urlConnection.setConnectTimeout(SOCKET_TIMEOUT_MS); 384 urlConnection.setReadTimeout(SOCKET_TIMEOUT_MS); 385 urlConnection.setUseCaches(false); 386 387 // Time how long it takes to get a response to our request 388 requestTimestamp = SystemClock.elapsedRealtime(); 389 390 urlConnection.getInputStream(); 391 392 // Time how long it takes to get a response to our request 393 long responseTimestamp = SystemClock.elapsedRealtime(); 394 395 // we got a valid response, but not from the real google 396 int rspCode = urlConnection.getResponseCode(); 397 boolean isCaptivePortal = rspCode != 204; 398 399 sendNetworkConditionsBroadcast(true /* response received */, isCaptivePortal, 400 requestTimestamp, responseTimestamp); 401 402 if (DBG) log("isCaptivePortal: ret=" + isCaptivePortal + " rspCode=" + rspCode); 403 return isCaptivePortal; 404 } catch (IOException e) { 405 if (DBG) log("Probably not a portal: exception " + e); 406 if (requestTimestamp != -1) { 407 sendFailedCaptivePortalCheckBroadcast(requestTimestamp); 408 } // else something went wrong with setting up the urlConnection 409 return false; 410 } finally { 411 if (urlConnection != null) { 412 urlConnection.disconnect(); 413 } 414 } 415 } 416 417 private InetAddress lookupHost(String hostname) { 418 InetAddress inetAddress[]; 419 try { 420 inetAddress = InetAddress.getAllByName(hostname); 421 } catch (UnknownHostException e) { 422 sendFailedCaptivePortalCheckBroadcast(SystemClock.elapsedRealtime()); 423 return null; 424 } 425 426 for (InetAddress a : inetAddress) { 427 if (a instanceof Inet4Address) return a; 428 } 429 430 sendFailedCaptivePortalCheckBroadcast(SystemClock.elapsedRealtime()); 431 return null; 432 } 433 434 private void sendFailedCaptivePortalCheckBroadcast(long requestTimestampMs) { 435 sendNetworkConditionsBroadcast(false /* response received */, false /* ignored */, 436 requestTimestampMs, 0 /* ignored */); 437 } 438 439 /** 440 * @param responseReceived - whether or not we received a valid HTTP response to our request. 441 * If false, isCaptivePortal and responseTimestampMs are ignored 442 */ 443 private void sendNetworkConditionsBroadcast(boolean responseReceived, boolean isCaptivePortal, 444 long requestTimestampMs, long responseTimestampMs) { 445 if (Settings.Global.getInt(mContext.getContentResolver(), 446 Settings.Global.WIFI_SCAN_ALWAYS_AVAILABLE, 0) == 0) { 447 if (DBG) log("Don't send network conditions - lacking user consent."); 448 return; 449 } 450 451 Intent latencyBroadcast = new Intent(ACTION_NETWORK_CONDITIONS_MEASURED); 452 switch (mNetworkInfo.getType()) { 453 case ConnectivityManager.TYPE_WIFI: 454 WifiInfo currentWifiInfo = mWifiManager.getConnectionInfo(); 455 if (currentWifiInfo != null) { 456 latencyBroadcast.putExtra(EXTRA_SSID, currentWifiInfo.getSSID()); 457 latencyBroadcast.putExtra(EXTRA_BSSID, currentWifiInfo.getBSSID()); 458 } else { 459 if (DBG) logw("network info is TYPE_WIFI but no ConnectionInfo found"); 460 return; 461 } 462 break; 463 case ConnectivityManager.TYPE_MOBILE: 464 latencyBroadcast.putExtra(EXTRA_NETWORK_TYPE, mTelephonyManager.getNetworkType()); 465 List<CellInfo> info = mTelephonyManager.getAllCellInfo(); 466 if (info == null) return; 467 StringBuffer uniqueCellId = new StringBuffer(); 468 int numRegisteredCellInfo = 0; 469 for (CellInfo cellInfo : info) { 470 if (cellInfo.isRegistered()) { 471 numRegisteredCellInfo++; 472 if (numRegisteredCellInfo > 1) { 473 if (DBG) log("more than one registered CellInfo. Can't " + 474 "tell which is active. Bailing."); 475 return; 476 } 477 if (cellInfo instanceof CellInfoCdma) { 478 CellIdentityCdma cellId = ((CellInfoCdma) cellInfo).getCellIdentity(); 479 latencyBroadcast.putExtra(EXTRA_CELL_ID, cellId); 480 } else if (cellInfo instanceof CellInfoGsm) { 481 CellIdentityGsm cellId = ((CellInfoGsm) cellInfo).getCellIdentity(); 482 latencyBroadcast.putExtra(EXTRA_CELL_ID, cellId); 483 } else if (cellInfo instanceof CellInfoLte) { 484 CellIdentityLte cellId = ((CellInfoLte) cellInfo).getCellIdentity(); 485 latencyBroadcast.putExtra(EXTRA_CELL_ID, cellId); 486 } else if (cellInfo instanceof CellInfoWcdma) { 487 CellIdentityWcdma cellId = ((CellInfoWcdma) cellInfo).getCellIdentity(); 488 latencyBroadcast.putExtra(EXTRA_CELL_ID, cellId); 489 } else { 490 if (DBG) logw("Registered cellinfo is unrecognized"); 491 return; 492 } 493 } 494 } 495 break; 496 default: 497 return; 498 } 499 latencyBroadcast.putExtra(EXTRA_CONNECTIVITY_TYPE, mNetworkInfo.getType()); 500 latencyBroadcast.putExtra(EXTRA_RESPONSE_RECEIVED, responseReceived); 501 latencyBroadcast.putExtra(EXTRA_REQUEST_TIMESTAMP_MS, requestTimestampMs); 502 503 if (responseReceived) { 504 latencyBroadcast.putExtra(EXTRA_IS_CAPTIVE_PORTAL, isCaptivePortal); 505 latencyBroadcast.putExtra(EXTRA_RESPONSE_TIMESTAMP_MS, responseTimestampMs); 506 } 507 mContext.sendBroadcast(latencyBroadcast, PERMISSION_ACCESS_NETWORK_CONDITIONS); 508 } 509 } 510