NetworkMonitor.java revision ca8f16ad14819ba17f5ff3d2e2bf6fbc9bbaa9f7
1/* 2 * Copyright (C) 2014 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 com.android.server.connectivity; 18 19import android.content.Context; 20import android.net.NetworkCapabilities; 21import android.net.NetworkInfo; 22import android.os.Handler; 23import android.os.Message; 24import android.os.SystemProperties; 25import android.provider.Settings; 26 27import com.android.internal.util.Protocol; 28import com.android.internal.util.State; 29import com.android.internal.util.StateMachine; 30import com.android.server.connectivity.NetworkAgentInfo; 31 32import java.io.BufferedReader; 33import java.io.InputStreamReader; 34import java.io.IOException; 35import java.io.OutputStreamWriter; 36import java.net.HttpURLConnection; 37import java.net.InetSocketAddress; 38import java.net.Socket; 39import java.net.URL; 40 41/** 42 * {@hide} 43 */ 44public class NetworkMonitor extends StateMachine { 45 private static final boolean DBG = true; 46 private static final String TAG = "NetworkMonitor"; 47 private static final String DEFAULT_SERVER = "clients3.google.com"; 48 private static final int SOCKET_TIMEOUT_MS = 10000; 49 50 private static final int BASE = Protocol.BASE_NETWORK_MONITOR; 51 52 /** 53 * Inform NetworkMonitor that their network is connected. 54 * Initiates Network Validation. 55 */ 56 public static final int CMD_NETWORK_CONNECTED = BASE + 1; 57 58 /** 59 * Inform ConnectivityService that the network is validated. 60 * obj = NetworkAgentInfo 61 */ 62 public static final int EVENT_NETWORK_VALIDATED = BASE + 2; 63 64 /** 65 * Inform NetworkMonitor to linger a network. The Monitor should 66 * start a timer and/or start watching for zero live connections while 67 * moving towards LINGER_COMPLETE. After the Linger period expires 68 * (or other events mark the end of the linger state) the LINGER_COMPLETE 69 * event should be sent and the network will be shut down. If a 70 * CMD_NETWORK_CONNECTED happens before the LINGER completes 71 * it indicates further desire to keep the network alive and so 72 * the LINGER is aborted. 73 */ 74 public static final int CMD_NETWORK_LINGER = BASE + 3; 75 76 /** 77 * Message to self indicating linger delay has expired. 78 * arg1 = Token to ignore old messages. 79 */ 80 private static final int CMD_LINGER_EXPIRED = BASE + 4; 81 82 /** 83 * Inform ConnectivityService that the network LINGER period has 84 * expired. 85 * obj = NetworkAgentInfo 86 */ 87 public static final int EVENT_NETWORK_LINGER_COMPLETE = BASE + 5; 88 89 /** 90 * Message to self indicating it's time to check for a captive portal again. 91 * TODO - Remove this once broadcast intents are used to communicate with 92 * apps to log into captive portals. 93 * arg1 = Token to ignore old messages. 94 */ 95 private static final int CMD_CAPTIVE_PORTAL_REEVALUATE = BASE + 6; 96 97 /** 98 * Message to self indicating it's time to evaluate a network's connectivity. 99 * arg1 = Token to ignore old messages. 100 */ 101 private static final int CMD_REEVALUATE = BASE + 7; 102 103 /** 104 * Message to self indicating network evaluation is complete. 105 * arg1 = Token to ignore old messages. 106 * arg2 = HTTP response code of network evaluation. 107 */ 108 private static final int EVENT_REEVALUATION_COMPLETE = BASE + 8; 109 110 /** 111 * Inform NetworkMonitor that the network has disconnected. 112 */ 113 public static final int CMD_NETWORK_DISCONNECTED = BASE + 9; 114 115 /** 116 * Force evaluation even if it has succeeded in the past. 117 */ 118 public static final int CMD_FORCE_REEVALUATION = BASE + 10; 119 120 private static final String LINGER_DELAY_PROPERTY = "persist.netmon.linger"; 121 // Default to 30s linger time-out. 122 private static final int DEFAULT_LINGER_DELAY_MS = 30000; 123 private final int mLingerDelayMs; 124 private int mLingerToken = 0; 125 126 private static final int CAPTIVE_PORTAL_REEVALUATE_DELAY_MS = 5000; 127 private int mCaptivePortalReevaluateToken = 0; 128 129 // Negative values disable reevaluation. 130 private static final String REEVALUATE_DELAY_PROPERTY = "persist.netmon.reeval_delay"; 131 // Default to 5s reevaluation delay. 132 private static final int DEFAULT_REEVALUATE_DELAY_MS = 5000; 133 private final int mReevaluateDelayMs; 134 private int mReevaluateToken = 0; 135 136 private final Context mContext; 137 private final Handler mConnectivityServiceHandler; 138 private final NetworkAgentInfo mNetworkAgentInfo; 139 140 private String mServer; 141 private boolean mIsCaptivePortalCheckEnabled = false; 142 143 private State mDefaultState = new DefaultState(); 144 private State mOfflineState = new OfflineState(); 145 private State mValidatedState = new ValidatedState(); 146 private State mEvaluatingState = new EvaluatingState(); 147 private State mCaptivePortalState = new CaptivePortalState(); 148 private State mLingeringState = new LingeringState(); 149 150 public NetworkMonitor(Context context, Handler handler, NetworkAgentInfo networkAgentInfo) { 151 // Add suffix indicating which NetworkMonitor we're talking about. 152 super(TAG + networkAgentInfo.name()); 153 154 mContext = context; 155 mConnectivityServiceHandler = handler; 156 mNetworkAgentInfo = networkAgentInfo; 157 158 addState(mDefaultState); 159 addState(mOfflineState, mDefaultState); 160 addState(mValidatedState, mDefaultState); 161 addState(mEvaluatingState, mDefaultState); 162 addState(mCaptivePortalState, mDefaultState); 163 addState(mLingeringState, mDefaultState); 164 setInitialState(mOfflineState); 165 166 mServer = Settings.Global.getString(mContext.getContentResolver(), 167 Settings.Global.CAPTIVE_PORTAL_SERVER); 168 if (mServer == null) mServer = DEFAULT_SERVER; 169 170 mLingerDelayMs = SystemProperties.getInt(LINGER_DELAY_PROPERTY, DEFAULT_LINGER_DELAY_MS); 171 mReevaluateDelayMs = SystemProperties.getInt(REEVALUATE_DELAY_PROPERTY, 172 DEFAULT_REEVALUATE_DELAY_MS); 173 174 // TODO: Enable this when we're ready. 175 // mIsCaptivePortalCheckEnabled = Settings.Global.getInt(mContext.getContentResolver(), 176 // Settings.Global.CAPTIVE_PORTAL_DETECTION_ENABLED, 1) == 1; 177 178 start(); 179 } 180 181 private class DefaultState extends State { 182 @Override 183 public boolean processMessage(Message message) { 184 if (DBG) log(getName() + message.toString()); 185 switch (message.what) { 186 case CMD_NETWORK_LINGER: 187 if (DBG) log("Lingering"); 188 transitionTo(mLingeringState); 189 break; 190 case CMD_NETWORK_CONNECTED: 191 if (DBG) log("Connected"); 192 transitionTo(mEvaluatingState); 193 break; 194 case CMD_NETWORK_DISCONNECTED: 195 if (DBG) log("Disconnected"); 196 transitionTo(mOfflineState); 197 break; 198 case CMD_FORCE_REEVALUATION: 199 if (DBG) log("Forcing reevaluation"); 200 transitionTo(mEvaluatingState); 201 break; 202 default: 203 break; 204 } 205 return HANDLED; 206 } 207 } 208 209 private class OfflineState extends State { 210 @Override 211 public boolean processMessage(Message message) { 212 if (DBG) log(getName() + message.toString()); 213 return NOT_HANDLED; 214 } 215 } 216 217 private class ValidatedState extends State { 218 @Override 219 public void enter() { 220 if (DBG) log("Validated"); 221 mConnectivityServiceHandler.sendMessage( 222 obtainMessage(EVENT_NETWORK_VALIDATED, mNetworkAgentInfo)); 223 } 224 225 @Override 226 public boolean processMessage(Message message) { 227 if (DBG) log(getName() + message.toString()); 228 switch (message.what) { 229 case CMD_NETWORK_CONNECTED: 230 transitionTo(mValidatedState); 231 break; 232 default: 233 return NOT_HANDLED; 234 } 235 return HANDLED; 236 } 237 } 238 239 private class EvaluatingState extends State { 240 private class EvaluateInternetConnectivity extends Thread { 241 private int mToken; 242 EvaluateInternetConnectivity(int token) { 243 mToken = token; 244 } 245 public void run() { 246 sendMessage(EVENT_REEVALUATION_COMPLETE, mToken, isCaptivePortal()); 247 } 248 } 249 250 @Override 251 public void enter() { 252 sendMessage(CMD_REEVALUATE, ++mReevaluateToken, 0); 253 } 254 255 @Override 256 public boolean processMessage(Message message) { 257 if (DBG) log(getName() + message.toString()); 258 switch (message.what) { 259 case CMD_REEVALUATE: 260 if (message.arg1 != mReevaluateToken) 261 break; 262 // If network provides no internet connectivity adjust evaluation. 263 if (mNetworkAgentInfo.networkCapabilities.hasCapability( 264 NetworkCapabilities.NET_CAPABILITY_INTERNET)) { 265 // TODO: Try to verify something works. Do all gateways respond to pings? 266 transitionTo(mValidatedState); 267 } 268 // Kick off a thread to perform internet connectivity evaluation. 269 Thread thread = new EvaluateInternetConnectivity(mReevaluateToken); 270 thread.run(); 271 break; 272 case EVENT_REEVALUATION_COMPLETE: 273 if (message.arg1 != mReevaluateToken) 274 break; 275 int httpResponseCode = message.arg2; 276 if (httpResponseCode == 204) { 277 transitionTo(mValidatedState); 278 } else if (httpResponseCode >= 200 && httpResponseCode <= 399) { 279 transitionTo(mCaptivePortalState); 280 } else { 281 if (mReevaluateDelayMs >= 0) { 282 Message msg = obtainMessage(CMD_REEVALUATE, ++mReevaluateToken, 0); 283 sendMessageDelayed(msg, mReevaluateDelayMs); 284 } 285 } 286 break; 287 default: 288 return NOT_HANDLED; 289 } 290 return HANDLED; 291 } 292 } 293 294 // TODO: Until we add an intent from the app handling captive portal 295 // login we'll just re-evaluate after a delay. 296 private class CaptivePortalState extends State { 297 @Override 298 public void enter() { 299 Message message = obtainMessage(CMD_CAPTIVE_PORTAL_REEVALUATE, 300 ++mCaptivePortalReevaluateToken, 0); 301 sendMessageDelayed(message, CAPTIVE_PORTAL_REEVALUATE_DELAY_MS); 302 } 303 304 @Override 305 public boolean processMessage(Message message) { 306 if (DBG) log(getName() + message.toString()); 307 switch (message.what) { 308 case CMD_CAPTIVE_PORTAL_REEVALUATE: 309 if (message.arg1 != mCaptivePortalReevaluateToken) 310 break; 311 transitionTo(mEvaluatingState); 312 break; 313 default: 314 return NOT_HANDLED; 315 } 316 return HANDLED; 317 } 318 } 319 320 private class LingeringState extends State { 321 @Override 322 public void enter() { 323 Message message = obtainMessage(CMD_LINGER_EXPIRED, ++mLingerToken, 0); 324 sendMessageDelayed(message, mLingerDelayMs); 325 } 326 327 @Override 328 public boolean processMessage(Message message) { 329 if (DBG) log(getName() + message.toString()); 330 switch (message.what) { 331 case CMD_NETWORK_CONNECTED: 332 // Go straight to active as we've already evaluated. 333 transitionTo(mValidatedState); 334 break; 335 case CMD_LINGER_EXPIRED: 336 if (message.arg1 != mLingerToken) 337 break; 338 mConnectivityServiceHandler.sendMessage( 339 obtainMessage(EVENT_NETWORK_LINGER_COMPLETE, mNetworkAgentInfo)); 340 break; 341 default: 342 return NOT_HANDLED; 343 } 344 return HANDLED; 345 } 346 } 347 348 /** 349 * Do a URL fetch on a known server to see if we get the data we expect. 350 * Returns HTTP response code. 351 */ 352 private int isCaptivePortal() { 353 if (!mIsCaptivePortalCheckEnabled) return 204; 354 355 String urlString = "http://" + mServer + "/generate_204"; 356 if (DBG) log("Checking " + urlString); 357 HttpURLConnection urlConnection = null; 358 Socket socket = null; 359 int httpResponseCode = 500; 360 try { 361 URL url = new URL(urlString); 362 if (false) { 363 // TODO: Need to add URLConnection.setNetwork() before we can enable. 364 urlConnection = (HttpURLConnection) url.openConnection(); 365 urlConnection.setInstanceFollowRedirects(false); 366 urlConnection.setConnectTimeout(SOCKET_TIMEOUT_MS); 367 urlConnection.setReadTimeout(SOCKET_TIMEOUT_MS); 368 urlConnection.setUseCaches(false); 369 urlConnection.getInputStream(); 370 httpResponseCode = urlConnection.getResponseCode(); 371 } else { 372 socket = new Socket(); 373 // TODO: setNetworkForSocket(socket, mNetworkAgentInfo.network.netId); 374 InetSocketAddress address = new InetSocketAddress(url.getHost(), 80); 375 // TODO: address = new InetSocketAddress( 376 // getByNameOnNetwork(mNetworkAgentInfo.network, url.getHost()), 80); 377 socket.connect(address); 378 BufferedReader reader = new BufferedReader( 379 new InputStreamReader(socket.getInputStream())); 380 OutputStreamWriter writer = new OutputStreamWriter(socket.getOutputStream()); 381 writer.write("GET " + url.getFile() + " HTTP/1.1\r\n\n"); 382 writer.flush(); 383 String response = reader.readLine(); 384 if (response.startsWith("HTTP/1.1 ")) { 385 httpResponseCode = Integer.parseInt(response.substring(9, 12)); 386 } 387 } 388 if (DBG) log("isCaptivePortal: ret=" + httpResponseCode); 389 } catch (IOException e) { 390 if (DBG) log("Probably not a portal: exception " + e); 391 } finally { 392 if (urlConnection != null) { 393 urlConnection.disconnect(); 394 } 395 if (socket != null) { 396 try { 397 socket.close(); 398 } catch (IOException e) { 399 // Ignore 400 } 401 } 402 } 403 return httpResponseCode; 404 } 405} 406