CaptivePortalTracker.java revision 35152f13b3302dbcb43345458dd5b8b092e37320
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.os.Handler; 32import android.os.UserHandle; 33import android.os.Message; 34import android.os.RemoteException; 35import android.provider.Settings; 36import android.telephony.TelephonyManager; 37import android.text.TextUtils; 38 39import com.android.internal.util.State; 40import com.android.internal.util.StateMachine; 41 42import java.io.IOException; 43import java.net.HttpURLConnection; 44import java.net.InetAddress; 45import java.net.Inet4Address; 46import java.net.SocketTimeoutException; 47import java.net.URL; 48import java.net.UnknownHostException; 49 50import com.android.internal.R; 51 52/** 53 * This class allows captive portal detection on a network. 54 * @hide 55 */ 56public class CaptivePortalTracker extends StateMachine { 57 private static final boolean DBG = true; 58 private static final String TAG = "CaptivePortalTracker"; 59 60 private static final String DEFAULT_SERVER = "clients3.google.com"; 61 private static final String NOTIFICATION_ID = "CaptivePortal.Notification"; 62 63 private static final int SOCKET_TIMEOUT_MS = 10000; 64 65 private String mServer; 66 private String mUrl; 67 private boolean mNotificationShown = false; 68 private boolean mIsCaptivePortalCheckEnabled = false; 69 private IConnectivityManager mConnService; 70 private TelephonyManager mTelephonyManager; 71 private Context mContext; 72 private NetworkInfo mNetworkInfo; 73 74 private static final int CMD_DETECT_PORTAL = 0; 75 private static final int CMD_CONNECTIVITY_CHANGE = 1; 76 private static final int CMD_DELAYED_CAPTIVE_CHECK = 2; 77 78 /* This delay happens every time before we do a captive check on a network */ 79 private static final int DELAYED_CHECK_INTERVAL_MS = 10000; 80 private int mDelayedCheckToken = 0; 81 82 private State mDefaultState = new DefaultState(); 83 private State mNoActiveNetworkState = new NoActiveNetworkState(); 84 private State mActiveNetworkState = new ActiveNetworkState(); 85 private State mDelayedCaptiveCheckState = new DelayedCaptiveCheckState(); 86 87 private static final String SETUP_WIZARD_PACKAGE = "com.google.android.setupwizard"; 88 private boolean mDeviceProvisioned = false; 89 private ProvisioningObserver mProvisioningObserver; 90 91 private CaptivePortalTracker(Context context, IConnectivityManager cs) { 92 super(TAG); 93 94 mContext = context; 95 mConnService = cs; 96 mTelephonyManager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE); 97 mProvisioningObserver = new ProvisioningObserver(); 98 99 IntentFilter filter = new IntentFilter(); 100 filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION); 101 filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION_IMMEDIATE); 102 mContext.registerReceiver(mReceiver, filter); 103 104 mServer = Settings.Global.getString(mContext.getContentResolver(), 105 Settings.Global.CAPTIVE_PORTAL_SERVER); 106 if (mServer == null) mServer = DEFAULT_SERVER; 107 108 mIsCaptivePortalCheckEnabled = Settings.Global.getInt(mContext.getContentResolver(), 109 Settings.Global.CAPTIVE_PORTAL_DETECTION_ENABLED, 1) == 1; 110 111 addState(mDefaultState); 112 addState(mNoActiveNetworkState, mDefaultState); 113 addState(mActiveNetworkState, mDefaultState); 114 addState(mDelayedCaptiveCheckState, mActiveNetworkState); 115 setInitialState(mNoActiveNetworkState); 116 } 117 118 private class ProvisioningObserver extends ContentObserver { 119 ProvisioningObserver() { 120 super(new Handler()); 121 mContext.getContentResolver().registerContentObserver(Settings.Global.getUriFor( 122 Settings.Global.DEVICE_PROVISIONED), false, this); 123 onChange(false); // load initial value 124 } 125 126 @Override 127 public void onChange(boolean selfChange) { 128 mDeviceProvisioned = Settings.Global.getInt(mContext.getContentResolver(), 129 Settings.Global.DEVICE_PROVISIONED, 0) != 0; 130 } 131 } 132 133 private final BroadcastReceiver mReceiver = new BroadcastReceiver() { 134 @Override 135 public void onReceive(Context context, Intent intent) { 136 String action = intent.getAction(); 137 // Normally, we respond to CONNECTIVITY_ACTION, allowing time for the change in 138 // connectivity to stabilize, but if the device is not yet provisioned, respond 139 // immediately to speed up transit through the setup wizard. 140 if ((mDeviceProvisioned && action.equals(ConnectivityManager.CONNECTIVITY_ACTION)) 141 || (!mDeviceProvisioned 142 && action.equals(ConnectivityManager.CONNECTIVITY_ACTION_IMMEDIATE))) { 143 NetworkInfo info = intent.getParcelableExtra( 144 ConnectivityManager.EXTRA_NETWORK_INFO); 145 sendMessage(obtainMessage(CMD_CONNECTIVITY_CHANGE, info)); 146 } 147 } 148 }; 149 150 public static CaptivePortalTracker makeCaptivePortalTracker(Context context, 151 IConnectivityManager cs) { 152 CaptivePortalTracker captivePortal = new CaptivePortalTracker(context, cs); 153 captivePortal.start(); 154 return captivePortal; 155 } 156 157 public void detectCaptivePortal(NetworkInfo info) { 158 sendMessage(obtainMessage(CMD_DETECT_PORTAL, info)); 159 } 160 161 private class DefaultState extends State { 162 @Override 163 public void enter() { 164 if (DBG) log(getName() + "\n"); 165 } 166 167 @Override 168 public boolean processMessage(Message message) { 169 if (DBG) log(getName() + message.toString() + "\n"); 170 switch (message.what) { 171 case CMD_DETECT_PORTAL: 172 NetworkInfo info = (NetworkInfo) message.obj; 173 // Checking on a secondary connection is not supported 174 // yet 175 notifyPortalCheckComplete(info); 176 break; 177 case CMD_CONNECTIVITY_CHANGE: 178 case CMD_DELAYED_CAPTIVE_CHECK: 179 break; 180 default: 181 loge("Ignoring " + message); 182 break; 183 } 184 return HANDLED; 185 } 186 } 187 188 private class NoActiveNetworkState extends State { 189 @Override 190 public void enter() { 191 if (DBG) log(getName() + "\n"); 192 mNetworkInfo = null; 193 /* Clear any previous notification */ 194 setNotificationVisible(false); 195 } 196 197 @Override 198 public boolean processMessage(Message message) { 199 if (DBG) log(getName() + message.toString() + "\n"); 200 InetAddress server; 201 NetworkInfo info; 202 switch (message.what) { 203 case CMD_CONNECTIVITY_CHANGE: 204 info = (NetworkInfo) message.obj; 205 if (info.isConnected() && isActiveNetwork(info)) { 206 mNetworkInfo = info; 207 transitionTo(mDelayedCaptiveCheckState); 208 } 209 break; 210 default: 211 return NOT_HANDLED; 212 } 213 return HANDLED; 214 } 215 } 216 217 private class ActiveNetworkState extends State { 218 @Override 219 public void enter() { 220 if (DBG) log(getName() + "\n"); 221 } 222 223 @Override 224 public boolean processMessage(Message message) { 225 NetworkInfo info; 226 switch (message.what) { 227 case CMD_CONNECTIVITY_CHANGE: 228 info = (NetworkInfo) message.obj; 229 if (!info.isConnected() 230 && info.getType() == mNetworkInfo.getType()) { 231 if (DBG) log("Disconnected from active network " + info); 232 transitionTo(mNoActiveNetworkState); 233 } else if (info.getType() != mNetworkInfo.getType() && 234 info.isConnected() && 235 isActiveNetwork(info)) { 236 if (DBG) log("Active network switched " + info); 237 deferMessage(message); 238 transitionTo(mNoActiveNetworkState); 239 } 240 break; 241 default: 242 return NOT_HANDLED; 243 } 244 return HANDLED; 245 } 246 } 247 248 249 250 private class DelayedCaptiveCheckState extends State { 251 @Override 252 public void enter() { 253 if (DBG) log(getName() + "\n"); 254 Message message = obtainMessage(CMD_DELAYED_CAPTIVE_CHECK, ++mDelayedCheckToken, 0); 255 if (mDeviceProvisioned) { 256 sendMessageDelayed(message, DELAYED_CHECK_INTERVAL_MS); 257 } else { 258 sendMessage(message); 259 } 260 } 261 262 @Override 263 public boolean processMessage(Message message) { 264 if (DBG) log(getName() + message.toString() + "\n"); 265 switch (message.what) { 266 case CMD_DELAYED_CAPTIVE_CHECK: 267 if (message.arg1 == mDelayedCheckToken) { 268 InetAddress server = lookupHost(mServer); 269 boolean captive = server != null && isCaptivePortal(server); 270 if (captive) { 271 if (DBG) log("Captive network " + mNetworkInfo); 272 } else { 273 if (DBG) log("Not captive network " + mNetworkInfo); 274 } 275 if (mDeviceProvisioned) { 276 if (captive) { 277 // Setup Wizard will assist the user in connecting to a captive 278 // portal, so make the notification visible unless during setup 279 setNotificationVisible(true); 280 } 281 } else { 282 Intent intent = new Intent( 283 ConnectivityManager.ACTION_CAPTIVE_PORTAL_TEST_COMPLETED); 284 intent.putExtra(ConnectivityManager.EXTRA_IS_CAPTIVE_PORTAL, captive); 285 intent.setPackage(SETUP_WIZARD_PACKAGE); 286 mContext.sendBroadcast(intent); 287 } 288 289 transitionTo(mActiveNetworkState); 290 } 291 break; 292 default: 293 return NOT_HANDLED; 294 } 295 return HANDLED; 296 } 297 } 298 299 private void notifyPortalCheckComplete(NetworkInfo info) { 300 if (info == null) { 301 loge("notifyPortalCheckComplete on null"); 302 return; 303 } 304 try { 305 mConnService.captivePortalCheckComplete(info); 306 } catch(RemoteException e) { 307 e.printStackTrace(); 308 } 309 } 310 311 private boolean isActiveNetwork(NetworkInfo info) { 312 try { 313 NetworkInfo active = mConnService.getActiveNetworkInfo(); 314 if (active != null && active.getType() == info.getType()) { 315 return true; 316 } 317 } catch (RemoteException e) { 318 e.printStackTrace(); 319 } 320 return false; 321 } 322 323 /** 324 * Do a URL fetch on a known server to see if we get the data we expect 325 */ 326 private boolean isCaptivePortal(InetAddress server) { 327 HttpURLConnection urlConnection = null; 328 if (!mIsCaptivePortalCheckEnabled) return false; 329 330 mUrl = "http://" + server.getHostAddress() + "/generate_204"; 331 if (DBG) log("Checking " + mUrl); 332 try { 333 URL url = new URL(mUrl); 334 urlConnection = (HttpURLConnection) url.openConnection(); 335 urlConnection.setInstanceFollowRedirects(false); 336 urlConnection.setConnectTimeout(SOCKET_TIMEOUT_MS); 337 urlConnection.setReadTimeout(SOCKET_TIMEOUT_MS); 338 urlConnection.setUseCaches(false); 339 urlConnection.getInputStream(); 340 // we got a valid response, but not from the real google 341 return urlConnection.getResponseCode() != 204; 342 } catch (SocketTimeoutException e) { 343 if (DBG) log("Probably a portal: exception " + e); 344 return true; 345 } catch (IOException e) { 346 if (DBG) log("Probably not a portal: exception " + e); 347 return false; 348 } finally { 349 if (urlConnection != null) { 350 urlConnection.disconnect(); 351 } 352 } 353 } 354 355 private InetAddress lookupHost(String hostname) { 356 InetAddress inetAddress[]; 357 try { 358 inetAddress = InetAddress.getAllByName(hostname); 359 } catch (UnknownHostException e) { 360 return null; 361 } 362 363 for (InetAddress a : inetAddress) { 364 if (a instanceof Inet4Address) return a; 365 } 366 return null; 367 } 368 369 private void setNotificationVisible(boolean visible) { 370 // if it should be hidden and it is already hidden, then noop 371 if (!visible && !mNotificationShown) { 372 if (DBG) log("setNotivicationVisible: false and not shown, so noop"); 373 return; 374 } 375 376 Resources r = Resources.getSystem(); 377 NotificationManager notificationManager = (NotificationManager) mContext 378 .getSystemService(Context.NOTIFICATION_SERVICE); 379 380 if (visible) { 381 CharSequence title; 382 CharSequence details; 383 int icon; 384 String url = null; 385 switch (mNetworkInfo.getType()) { 386 case ConnectivityManager.TYPE_WIFI: 387 title = r.getString(R.string.wifi_available_sign_in, 0); 388 details = r.getString(R.string.network_available_sign_in_detailed, 389 mNetworkInfo.getExtraInfo()); 390 icon = R.drawable.stat_notify_wifi_in_range; 391 url = mUrl; 392 break; 393 case ConnectivityManager.TYPE_MOBILE: 394 title = r.getString(R.string.network_available_sign_in, 0); 395 // TODO: Change this to pull from NetworkInfo once a printable 396 // name has been added to it 397 details = mTelephonyManager.getNetworkOperatorName(); 398 icon = R.drawable.stat_notify_rssi_in_range; 399 try { 400 url = mConnService.getMobileProvisioningUrl(); 401 if (TextUtils.isEmpty(url)) { 402 url = mConnService.getMobileRedirectedProvisioningUrl(); 403 } 404 } catch(RemoteException e) { 405 e.printStackTrace(); 406 } 407 if (TextUtils.isEmpty(url)) { 408 url = mUrl; 409 } 410 break; 411 default: 412 title = r.getString(R.string.network_available_sign_in, 0); 413 details = r.getString(R.string.network_available_sign_in_detailed, 414 mNetworkInfo.getExtraInfo()); 415 icon = R.drawable.stat_notify_rssi_in_range; 416 url = mUrl; 417 break; 418 } 419 420 Notification notification = new Notification(); 421 notification.when = 0; 422 notification.icon = icon; 423 notification.flags = Notification.FLAG_AUTO_CANCEL; 424 Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); 425 intent.setFlags(Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT | 426 Intent.FLAG_ACTIVITY_NEW_TASK); 427 notification.contentIntent = PendingIntent.getActivity(mContext, 0, intent, 0); 428 notification.tickerText = title; 429 notification.setLatestEventInfo(mContext, title, details, notification.contentIntent); 430 431 if (DBG) log("setNotivicationVisible: make visible"); 432 notificationManager.notify(NOTIFICATION_ID, 1, notification); 433 } else { 434 if (DBG) log("setNotivicationVisible: cancel notification"); 435 notificationManager.cancel(NOTIFICATION_ID, 1); 436 } 437 mNotificationShown = visible; 438 } 439} 440