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