CaptivePortalTracker.java revision da6da0907b28d4704aabbdb1bbeb4300954670d1
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.Notification; 20import android.app.NotificationManager; 21import android.app.PendingIntent; 22import android.content.BroadcastReceiver; 23import android.content.Context; 24import android.content.Intent; 25import android.content.IntentFilter; 26import android.content.res.Resources; 27import android.database.ContentObserver; 28import android.net.ConnectivityManager; 29import android.net.IConnectivityManager; 30import android.os.Handler; 31import android.os.HandlerThread; 32import android.os.Looper; 33import android.os.Message; 34import android.os.RemoteException; 35import android.provider.Settings; 36import android.util.Log; 37 38import java.io.IOException; 39import java.net.HttpURLConnection; 40import java.net.InetAddress; 41import java.net.Inet4Address; 42import java.net.URL; 43import java.net.UnknownHostException; 44import java.util.concurrent.atomic.AtomicBoolean; 45 46import com.android.internal.R; 47 48/** 49 * This class allows captive portal detection 50 * @hide 51 */ 52public class CaptivePortalTracker { 53 private static final boolean DBG = true; 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 InternalHandler mHandler; 66 private IConnectivityManager mConnService; 67 private Context mContext; 68 private NetworkInfo mNetworkInfo; 69 private boolean mIsCaptivePortal = false; 70 71 private static final int DETECT_PORTAL = 0; 72 private static final int HANDLE_CONNECT = 1; 73 74 /** 75 * Activity Action: Switch to the captive portal network 76 * <p>Input: Nothing. 77 * <p>Output: Nothing. 78 */ 79 public static final String ACTION_SWITCH_TO_CAPTIVE_PORTAL 80 = "android.net.SWITCH_TO_CAPTIVE_PORTAL"; 81 82 private CaptivePortalTracker(Context context, NetworkInfo info, IConnectivityManager cs) { 83 mContext = context; 84 mNetworkInfo = info; 85 mConnService = cs; 86 87 HandlerThread handlerThread = new HandlerThread("CaptivePortalThread"); 88 handlerThread.start(); 89 mHandler = new InternalHandler(handlerThread.getLooper()); 90 mHandler.obtainMessage(DETECT_PORTAL).sendToTarget(); 91 92 IntentFilter filter = new IntentFilter(); 93 filter.addAction(ACTION_SWITCH_TO_CAPTIVE_PORTAL); 94 filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION); 95 96 mContext.registerReceiver(mReceiver, filter); 97 98 mServer = Settings.Secure.getString(mContext.getContentResolver(), 99 Settings.Secure.CAPTIVE_PORTAL_SERVER); 100 if (mServer == null) mServer = DEFAULT_SERVER; 101 102 mIsCaptivePortalCheckEnabled = Settings.Secure.getInt(mContext.getContentResolver(), 103 Settings.Secure.CAPTIVE_PORTAL_DETECTION_ENABLED, 1) == 1; 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(ACTION_SWITCH_TO_CAPTIVE_PORTAL)) { 111 notifyPortalCheckComplete(); 112 } else if (action.equals(ConnectivityManager.CONNECTIVITY_ACTION)) { 113 NetworkInfo info = intent.getParcelableExtra( 114 ConnectivityManager.EXTRA_NETWORK_INFO); 115 mHandler.obtainMessage(HANDLE_CONNECT, info).sendToTarget(); 116 } 117 } 118 }; 119 120 public static CaptivePortalTracker detect(Context context, NetworkInfo info, 121 IConnectivityManager cs) { 122 CaptivePortalTracker captivePortal = new CaptivePortalTracker(context, info, cs); 123 return captivePortal; 124 } 125 126 private class InternalHandler extends Handler { 127 public InternalHandler(Looper looper) { 128 super(looper); 129 } 130 131 @Override 132 public void handleMessage(Message msg) { 133 switch (msg.what) { 134 case DETECT_PORTAL: 135 InetAddress server = lookupHost(mServer); 136 if (server != null) { 137 requestRouteToHost(server); 138 if (isCaptivePortal(server)) { 139 if (DBG) log("Captive portal " + mNetworkInfo); 140 setNotificationVisible(true); 141 mIsCaptivePortal = true; 142 break; 143 } 144 } 145 notifyPortalCheckComplete(); 146 quit(); 147 break; 148 case HANDLE_CONNECT: 149 NetworkInfo info = (NetworkInfo) msg.obj; 150 if (info.getType() != mNetworkInfo.getType()) break; 151 152 if (info.getState() == NetworkInfo.State.CONNECTED || 153 info.getState() == NetworkInfo.State.DISCONNECTED) { 154 setNotificationVisible(false); 155 } 156 157 /* Connected to a captive portal */ 158 if (info.getState() == NetworkInfo.State.CONNECTED && 159 mIsCaptivePortal) { 160 launchBrowser(); 161 quit(); 162 } 163 break; 164 default: 165 loge("Unhandled message " + msg); 166 break; 167 } 168 } 169 170 private void quit() { 171 mIsCaptivePortal = false; 172 getLooper().quit(); 173 mContext.unregisterReceiver(mReceiver); 174 } 175 } 176 177 private void launchBrowser() { 178 Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(mUrl)); 179 intent.setFlags(Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT | Intent.FLAG_ACTIVITY_NEW_TASK); 180 mContext.startActivity(intent); 181 } 182 183 private void notifyPortalCheckComplete() { 184 try { 185 mConnService.captivePortalCheckComplete(mNetworkInfo); 186 } catch(RemoteException e) { 187 e.printStackTrace(); 188 } 189 } 190 191 private void requestRouteToHost(InetAddress server) { 192 try { 193 mConnService.requestRouteToHostAddress(mNetworkInfo.getType(), 194 server.getAddress()); 195 } catch (RemoteException e) { 196 e.printStackTrace(); 197 } 198 } 199 200 /** 201 * Do a URL fetch on a known server to see if we get the data we expect 202 */ 203 private boolean isCaptivePortal(InetAddress server) { 204 HttpURLConnection urlConnection = null; 205 if (!mIsCaptivePortalCheckEnabled) return false; 206 207 mUrl = "http://" + server.getHostAddress() + "/generate_204"; 208 try { 209 URL url = new URL(mUrl); 210 urlConnection = (HttpURLConnection) url.openConnection(); 211 urlConnection.setInstanceFollowRedirects(false); 212 urlConnection.setConnectTimeout(SOCKET_TIMEOUT_MS); 213 urlConnection.setReadTimeout(SOCKET_TIMEOUT_MS); 214 urlConnection.setUseCaches(false); 215 urlConnection.getInputStream(); 216 // we got a valid response, but not from the real google 217 return urlConnection.getResponseCode() != 204; 218 } catch (IOException e) { 219 if (DBG) log("Probably not a portal: exception " + e); 220 return false; 221 } finally { 222 if (urlConnection != null) { 223 urlConnection.disconnect(); 224 } 225 } 226 } 227 228 private InetAddress lookupHost(String hostname) { 229 InetAddress inetAddress[]; 230 try { 231 inetAddress = InetAddress.getAllByName(hostname); 232 } catch (UnknownHostException e) { 233 return null; 234 } 235 236 for (InetAddress a : inetAddress) { 237 if (a instanceof Inet4Address) return a; 238 } 239 return null; 240 } 241 242 private void setNotificationVisible(boolean visible) { 243 // if it should be hidden and it is already hidden, then noop 244 if (!visible && !mNotificationShown) { 245 return; 246 } 247 248 Resources r = Resources.getSystem(); 249 NotificationManager notificationManager = (NotificationManager) mContext 250 .getSystemService(Context.NOTIFICATION_SERVICE); 251 252 if (visible) { 253 CharSequence title = r.getString(R.string.wifi_available_sign_in, 0); 254 CharSequence details = r.getString(R.string.wifi_available_sign_in_detailed, 255 mNetworkInfo.getExtraInfo()); 256 257 Notification notification = new Notification(); 258 notification.when = 0; 259 notification.icon = com.android.internal.R.drawable.stat_notify_wifi_in_range; 260 notification.flags = Notification.FLAG_AUTO_CANCEL; 261 notification.contentIntent = PendingIntent.getBroadcast(mContext, 0, 262 new Intent(CaptivePortalTracker.ACTION_SWITCH_TO_CAPTIVE_PORTAL), 0); 263 264 notification.tickerText = title; 265 notification.setLatestEventInfo(mContext, title, details, notification.contentIntent); 266 267 notificationManager.notify(NOTIFICATION_ID, 1, notification); 268 } else { 269 notificationManager.cancel(NOTIFICATION_ID, 1); 270 } 271 mNotificationShown = visible; 272 } 273 274 private static void log(String s) { 275 Log.d(TAG, s); 276 } 277 278 private static void loge(String s) { 279 Log.e(TAG, s); 280 } 281 282} 283