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