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