blob: 47789b1f1f87797a37de2f28b4c530d956312d6d [file] [log] [blame]
Paul Jensenca8f16a2014-05-09 12:47:55 -04001/*
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}