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