blob: 0d3b50183c747d5153bce6668c3342902b9a8676 [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
Paul Jensen869868be2014-05-15 10:33:05 -040019import android.app.PendingIntent;
20import android.content.BroadcastReceiver;
21import android.content.ComponentName;
Paul Jensenca8f16a2014-05-09 12:47:55 -040022import android.content.Context;
Paul Jensen869868be2014-05-15 10:33:05 -040023import android.content.Intent;
24import android.content.IntentFilter;
25import android.net.ConnectivityManager;
26import android.net.Network;
Paul Jensenca8f16a2014-05-09 12:47:55 -040027import android.net.NetworkCapabilities;
28import android.net.NetworkInfo;
29import android.os.Handler;
30import android.os.Message;
31import android.os.SystemProperties;
Paul Jensen869868be2014-05-15 10:33:05 -040032import android.os.UserHandle;
Paul Jensenca8f16a2014-05-09 12:47:55 -040033import android.provider.Settings;
34
35import com.android.internal.util.Protocol;
36import com.android.internal.util.State;
37import com.android.internal.util.StateMachine;
Paul Jensen869868be2014-05-15 10:33:05 -040038import com.android.server.ConnectivityService;
Paul Jensenca8f16a2014-05-09 12:47:55 -040039import com.android.server.connectivity.NetworkAgentInfo;
40
41import java.io.BufferedReader;
42import java.io.InputStreamReader;
43import java.io.IOException;
44import java.io.OutputStreamWriter;
45import java.net.HttpURLConnection;
Paul Jensen869868be2014-05-15 10:33:05 -040046import java.net.InetAddress;
Paul Jensenca8f16a2014-05-09 12:47:55 -040047import java.net.InetSocketAddress;
48import java.net.Socket;
49import java.net.URL;
50
51/**
52 * {@hide}
53 */
54public class NetworkMonitor extends StateMachine {
55 private static final boolean DBG = true;
56 private static final String TAG = "NetworkMonitor";
57 private static final String DEFAULT_SERVER = "clients3.google.com";
58 private static final int SOCKET_TIMEOUT_MS = 10000;
59
Paul Jensen869868be2014-05-15 10:33:05 -040060 // Intent broadcast when user selects sign-in notification.
61 private static final String ACTION_SIGN_IN_REQUESTED =
62 "android.net.netmon.sign_in_requested";
63
64 // Keep these in sync with CaptivePortalLoginActivity.java.
65 // Intent broadcast from CaptivePortalLogin indicating sign-in is complete.
66 // Extras:
67 // EXTRA_TEXT = netId
68 // LOGGED_IN_RESULT = "1" if we should use network, "0" if not.
69 private static final String ACTION_CAPTIVE_PORTAL_LOGGED_IN =
70 "android.net.netmon.captive_portal_logged_in";
71 private static final String LOGGED_IN_RESULT = "result";
72
Paul Jensenca8f16a2014-05-09 12:47:55 -040073 private static final int BASE = Protocol.BASE_NETWORK_MONITOR;
74
75 /**
76 * Inform NetworkMonitor that their network is connected.
77 * Initiates Network Validation.
78 */
79 public static final int CMD_NETWORK_CONNECTED = BASE + 1;
80
81 /**
82 * Inform ConnectivityService that the network is validated.
83 * obj = NetworkAgentInfo
84 */
85 public static final int EVENT_NETWORK_VALIDATED = BASE + 2;
86
87 /**
88 * Inform NetworkMonitor to linger a network. The Monitor should
89 * start a timer and/or start watching for zero live connections while
90 * moving towards LINGER_COMPLETE. After the Linger period expires
91 * (or other events mark the end of the linger state) the LINGER_COMPLETE
92 * event should be sent and the network will be shut down. If a
93 * CMD_NETWORK_CONNECTED happens before the LINGER completes
94 * it indicates further desire to keep the network alive and so
95 * the LINGER is aborted.
96 */
97 public static final int CMD_NETWORK_LINGER = BASE + 3;
98
99 /**
100 * Message to self indicating linger delay has expired.
101 * arg1 = Token to ignore old messages.
102 */
103 private static final int CMD_LINGER_EXPIRED = BASE + 4;
104
105 /**
106 * Inform ConnectivityService that the network LINGER period has
107 * expired.
108 * obj = NetworkAgentInfo
109 */
110 public static final int EVENT_NETWORK_LINGER_COMPLETE = BASE + 5;
111
112 /**
Paul Jensenca8f16a2014-05-09 12:47:55 -0400113 * Message to self indicating it's time to evaluate a network's connectivity.
114 * arg1 = Token to ignore old messages.
115 */
Paul Jensen869868be2014-05-15 10:33:05 -0400116 private static final int CMD_REEVALUATE = BASE + 6;
Paul Jensenca8f16a2014-05-09 12:47:55 -0400117
118 /**
119 * Message to self indicating network evaluation is complete.
120 * arg1 = Token to ignore old messages.
121 * arg2 = HTTP response code of network evaluation.
122 */
Paul Jensen869868be2014-05-15 10:33:05 -0400123 private static final int EVENT_REEVALUATION_COMPLETE = BASE + 7;
Paul Jensenca8f16a2014-05-09 12:47:55 -0400124
125 /**
126 * Inform NetworkMonitor that the network has disconnected.
127 */
Paul Jensen869868be2014-05-15 10:33:05 -0400128 public static final int CMD_NETWORK_DISCONNECTED = BASE + 8;
Paul Jensenca8f16a2014-05-09 12:47:55 -0400129
130 /**
131 * Force evaluation even if it has succeeded in the past.
132 */
Paul Jensen869868be2014-05-15 10:33:05 -0400133 public static final int CMD_FORCE_REEVALUATION = BASE + 9;
134
135 /**
136 * Message to self indicating captive portal login is complete.
137 * arg1 = Token to ignore old messages.
138 * arg2 = 1 if we should use this network, 0 otherwise.
139 */
140 private static final int CMD_CAPTIVE_PORTAL_LOGGED_IN = BASE + 10;
141
142 /**
143 * Message to self indicating user desires to log into captive portal.
144 * arg1 = Token to ignore old messages.
145 */
146 private static final int CMD_USER_WANTS_SIGN_IN = BASE + 11;
147
148 /**
149 * Request ConnectivityService display provisioning notification.
150 * arg1 = Whether to make the notification visible.
151 * obj = Intent to be launched when notification selected by user.
152 * replyTo = NetworkAgentInfo.messenger so ConnectivityService can identify sender.
153 */
154 public static final int EVENT_PROVISIONING_NOTIFICATION = BASE + 12;
155
156 /**
157 * Message to self indicating sign-in app bypassed captive portal.
158 */
159 private static final int EVENT_APP_BYPASSED_CAPTIVE_PORTAL = BASE + 13;
160
161 /**
162 * Message to self indicating no sign-in app responded.
163 */
164 private static final int EVENT_NO_APP_RESPONSE = BASE + 14;
165
166 /**
167 * Message to self indicating sign-in app indicates sign-in is not possible.
168 */
169 private static final int EVENT_APP_INDICATES_SIGN_IN_IMPOSSIBLE = BASE + 15;
Paul Jensenca8f16a2014-05-09 12:47:55 -0400170
171 private static final String LINGER_DELAY_PROPERTY = "persist.netmon.linger";
172 // Default to 30s linger time-out.
173 private static final int DEFAULT_LINGER_DELAY_MS = 30000;
174 private final int mLingerDelayMs;
175 private int mLingerToken = 0;
176
Paul Jensenca8f16a2014-05-09 12:47:55 -0400177 // Negative values disable reevaluation.
178 private static final String REEVALUATE_DELAY_PROPERTY = "persist.netmon.reeval_delay";
179 // Default to 5s reevaluation delay.
180 private static final int DEFAULT_REEVALUATE_DELAY_MS = 5000;
Paul Jensen869868be2014-05-15 10:33:05 -0400181 private static final int MAX_RETRIES = 10;
Paul Jensenca8f16a2014-05-09 12:47:55 -0400182 private final int mReevaluateDelayMs;
183 private int mReevaluateToken = 0;
184
Paul Jensen869868be2014-05-15 10:33:05 -0400185 private int mCaptivePortalLoggedInToken = 0;
186 private int mUserPromptedToken = 0;
187
Paul Jensenca8f16a2014-05-09 12:47:55 -0400188 private final Context mContext;
189 private final Handler mConnectivityServiceHandler;
190 private final NetworkAgentInfo mNetworkAgentInfo;
191
192 private String mServer;
193 private boolean mIsCaptivePortalCheckEnabled = false;
194
195 private State mDefaultState = new DefaultState();
196 private State mOfflineState = new OfflineState();
197 private State mValidatedState = new ValidatedState();
198 private State mEvaluatingState = new EvaluatingState();
Paul Jensen869868be2014-05-15 10:33:05 -0400199 private State mUninteractiveAppsPromptedState = new UninteractiveAppsPromptedState();
200 private State mUserPromptedState = new UserPromptedState();
201 private State mInteractiveAppsPromptedState = new InteractiveAppsPromptedState();
Paul Jensenca8f16a2014-05-09 12:47:55 -0400202 private State mCaptivePortalState = new CaptivePortalState();
203 private State mLingeringState = new LingeringState();
204
205 public NetworkMonitor(Context context, Handler handler, NetworkAgentInfo networkAgentInfo) {
206 // Add suffix indicating which NetworkMonitor we're talking about.
207 super(TAG + networkAgentInfo.name());
208
209 mContext = context;
210 mConnectivityServiceHandler = handler;
211 mNetworkAgentInfo = networkAgentInfo;
212
213 addState(mDefaultState);
214 addState(mOfflineState, mDefaultState);
215 addState(mValidatedState, mDefaultState);
216 addState(mEvaluatingState, mDefaultState);
Paul Jensen869868be2014-05-15 10:33:05 -0400217 addState(mUninteractiveAppsPromptedState, mDefaultState);
218 addState(mUserPromptedState, mDefaultState);
219 addState(mInteractiveAppsPromptedState, mDefaultState);
Paul Jensenca8f16a2014-05-09 12:47:55 -0400220 addState(mCaptivePortalState, mDefaultState);
221 addState(mLingeringState, mDefaultState);
222 setInitialState(mOfflineState);
223
224 mServer = Settings.Global.getString(mContext.getContentResolver(),
225 Settings.Global.CAPTIVE_PORTAL_SERVER);
226 if (mServer == null) mServer = DEFAULT_SERVER;
227
228 mLingerDelayMs = SystemProperties.getInt(LINGER_DELAY_PROPERTY, DEFAULT_LINGER_DELAY_MS);
229 mReevaluateDelayMs = SystemProperties.getInt(REEVALUATE_DELAY_PROPERTY,
230 DEFAULT_REEVALUATE_DELAY_MS);
231
Paul Jensen869868be2014-05-15 10:33:05 -0400232 mIsCaptivePortalCheckEnabled = Settings.Global.getInt(mContext.getContentResolver(),
233 Settings.Global.CAPTIVE_PORTAL_DETECTION_ENABLED, 1) == 1;
Paul Jensenca8f16a2014-05-09 12:47:55 -0400234
235 start();
236 }
237
238 private class DefaultState extends State {
239 @Override
240 public boolean processMessage(Message message) {
241 if (DBG) log(getName() + message.toString());
242 switch (message.what) {
243 case CMD_NETWORK_LINGER:
244 if (DBG) log("Lingering");
245 transitionTo(mLingeringState);
246 break;
247 case CMD_NETWORK_CONNECTED:
248 if (DBG) log("Connected");
249 transitionTo(mEvaluatingState);
250 break;
251 case CMD_NETWORK_DISCONNECTED:
252 if (DBG) log("Disconnected");
253 transitionTo(mOfflineState);
254 break;
255 case CMD_FORCE_REEVALUATION:
256 if (DBG) log("Forcing reevaluation");
257 transitionTo(mEvaluatingState);
258 break;
259 default:
260 break;
261 }
262 return HANDLED;
263 }
264 }
265
266 private class OfflineState extends State {
267 @Override
268 public boolean processMessage(Message message) {
269 if (DBG) log(getName() + message.toString());
270 return NOT_HANDLED;
271 }
272 }
273
274 private class ValidatedState extends State {
275 @Override
276 public void enter() {
277 if (DBG) log("Validated");
278 mConnectivityServiceHandler.sendMessage(
279 obtainMessage(EVENT_NETWORK_VALIDATED, mNetworkAgentInfo));
280 }
281
282 @Override
283 public boolean processMessage(Message message) {
284 if (DBG) log(getName() + message.toString());
285 switch (message.what) {
286 case CMD_NETWORK_CONNECTED:
287 transitionTo(mValidatedState);
288 break;
289 default:
290 return NOT_HANDLED;
291 }
292 return HANDLED;
293 }
294 }
295
296 private class EvaluatingState extends State {
Paul Jensen869868be2014-05-15 10:33:05 -0400297 private int mRetries;
298
Paul Jensenca8f16a2014-05-09 12:47:55 -0400299 private class EvaluateInternetConnectivity extends Thread {
300 private int mToken;
301 EvaluateInternetConnectivity(int token) {
302 mToken = token;
303 }
304 public void run() {
305 sendMessage(EVENT_REEVALUATION_COMPLETE, mToken, isCaptivePortal());
306 }
307 }
308
309 @Override
310 public void enter() {
Paul Jensen869868be2014-05-15 10:33:05 -0400311 mRetries = 0;
Paul Jensenca8f16a2014-05-09 12:47:55 -0400312 sendMessage(CMD_REEVALUATE, ++mReevaluateToken, 0);
313 }
314
315 @Override
316 public boolean processMessage(Message message) {
317 if (DBG) log(getName() + message.toString());
318 switch (message.what) {
319 case CMD_REEVALUATE:
320 if (message.arg1 != mReevaluateToken)
321 break;
322 // If network provides no internet connectivity adjust evaluation.
Paul Jensen869868be2014-05-15 10:33:05 -0400323 if (!mNetworkAgentInfo.networkCapabilities.hasCapability(
Paul Jensenca8f16a2014-05-09 12:47:55 -0400324 NetworkCapabilities.NET_CAPABILITY_INTERNET)) {
325 // TODO: Try to verify something works. Do all gateways respond to pings?
326 transitionTo(mValidatedState);
327 }
328 // Kick off a thread to perform internet connectivity evaluation.
329 Thread thread = new EvaluateInternetConnectivity(mReevaluateToken);
330 thread.run();
331 break;
332 case EVENT_REEVALUATION_COMPLETE:
333 if (message.arg1 != mReevaluateToken)
334 break;
335 int httpResponseCode = message.arg2;
336 if (httpResponseCode == 204) {
337 transitionTo(mValidatedState);
338 } else if (httpResponseCode >= 200 && httpResponseCode <= 399) {
Paul Jensen869868be2014-05-15 10:33:05 -0400339 transitionTo(mUninteractiveAppsPromptedState);
340 } else if (++mRetries > MAX_RETRIES) {
341 transitionTo(mOfflineState);
342 } else if (mReevaluateDelayMs >= 0) {
343 Message msg = obtainMessage(CMD_REEVALUATE, ++mReevaluateToken, 0);
344 sendMessageDelayed(msg, mReevaluateDelayMs);
Paul Jensenca8f16a2014-05-09 12:47:55 -0400345 }
346 break;
347 default:
348 return NOT_HANDLED;
349 }
350 return HANDLED;
351 }
352 }
353
Paul Jensen869868be2014-05-15 10:33:05 -0400354 private class AppRespondedBroadcastReceiver extends BroadcastReceiver {
355 private static final int CAPTIVE_PORTAL_UNINITIALIZED_RETURN_CODE = 0;
356 private boolean mCanceled;
357 AppRespondedBroadcastReceiver() {
358 mCanceled = false;
359 }
360 public void send(String action) {
361 Intent intent = new Intent(action);
362 intent.putExtra(ConnectivityManager.EXTRA_NETWORK, mNetworkAgentInfo.network);
363 mContext.sendOrderedBroadcastAsUser(intent, UserHandle.ALL, null, this, getHandler(),
364 CAPTIVE_PORTAL_UNINITIALIZED_RETURN_CODE, null, null);
365 }
366 public void cancel() {
367 mCanceled = true;
368 }
369 @Override
370 public void onReceive(Context context, Intent intent) {
371 if (!mCanceled) {
372 cancel();
373 switch (getResultCode()) {
374 case ConnectivityManager.CAPTIVE_PORTAL_SIGNED_IN:
375 sendMessage(EVENT_APP_BYPASSED_CAPTIVE_PORTAL);
376 break;
377 case ConnectivityManager.CAPTIVE_PORTAL_DISCONNECT:
378 sendMessage(EVENT_APP_INDICATES_SIGN_IN_IMPOSSIBLE);
379 break;
380 // NOTE: This case label makes compiler enforce no overlap between result codes.
381 case CAPTIVE_PORTAL_UNINITIALIZED_RETURN_CODE:
382 default:
383 sendMessage(EVENT_NO_APP_RESPONSE);
384 break;
385 }
386 }
387 }
388 }
389
390 private class UninteractiveAppsPromptedState extends State {
391 private AppRespondedBroadcastReceiver mReceiver;
Paul Jensenca8f16a2014-05-09 12:47:55 -0400392 @Override
393 public void enter() {
Paul Jensen869868be2014-05-15 10:33:05 -0400394 mReceiver = new AppRespondedBroadcastReceiver();
395 mReceiver.send(ConnectivityManager.ACTION_CAPTIVE_PORTAL_DETECTED);
396 }
397 @Override
398 public boolean processMessage(Message message) {
399 if (DBG) log(getName() + message.toString());
400 switch (message.what) {
401 case EVENT_APP_BYPASSED_CAPTIVE_PORTAL:
402 transitionTo(mValidatedState);
403 break;
404 case EVENT_APP_INDICATES_SIGN_IN_IMPOSSIBLE:
405 transitionTo(mOfflineState);
406 break;
407 case EVENT_NO_APP_RESPONSE:
408 transitionTo(mUserPromptedState);
409 break;
410 default:
411 return NOT_HANDLED;
412 }
413 return HANDLED;
414 }
415 public void exit() {
416 mReceiver.cancel();
417 }
418 }
419
420 private class UserPromptedState extends State {
421 private class UserRespondedBroadcastReceiver extends BroadcastReceiver {
422 private final int mToken;
423 UserRespondedBroadcastReceiver(int token) {
424 mToken = token;
425 }
426 @Override
427 public void onReceive(Context context, Intent intent) {
428 if (Integer.parseInt(intent.getStringExtra(Intent.EXTRA_TEXT)) ==
429 mNetworkAgentInfo.network.netId) {
430 sendMessage(obtainMessage(CMD_USER_WANTS_SIGN_IN, mToken));
431 }
432 }
433 }
434
435 private UserRespondedBroadcastReceiver mUserRespondedBroadcastReceiver;
436
437 @Override
438 public void enter() {
439 // Wait for user to select sign-in notifcation.
440 mUserRespondedBroadcastReceiver = new UserRespondedBroadcastReceiver(
441 ++mUserPromptedToken);
442 IntentFilter filter = new IntentFilter(ACTION_SIGN_IN_REQUESTED);
443 mContext.registerReceiver(mUserRespondedBroadcastReceiver, filter);
444 // Initiate notification to sign-in.
445 Intent intent = new Intent(ACTION_SIGN_IN_REQUESTED);
446 intent.putExtra(Intent.EXTRA_TEXT, String.valueOf(mNetworkAgentInfo.network.netId));
447 Message message = obtainMessage(EVENT_PROVISIONING_NOTIFICATION, 1, 0,
448 PendingIntent.getBroadcast(mContext, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT));
449 message.replyTo = mNetworkAgentInfo.messenger;
450 mConnectivityServiceHandler.sendMessage(message);
Paul Jensenca8f16a2014-05-09 12:47:55 -0400451 }
452
453 @Override
454 public boolean processMessage(Message message) {
455 if (DBG) log(getName() + message.toString());
456 switch (message.what) {
Paul Jensen869868be2014-05-15 10:33:05 -0400457 case CMD_USER_WANTS_SIGN_IN:
458 if (message.arg1 != mUserPromptedToken)
Paul Jensenca8f16a2014-05-09 12:47:55 -0400459 break;
Paul Jensen869868be2014-05-15 10:33:05 -0400460 transitionTo(mInteractiveAppsPromptedState);
Paul Jensenca8f16a2014-05-09 12:47:55 -0400461 break;
462 default:
463 return NOT_HANDLED;
464 }
465 return HANDLED;
466 }
Paul Jensen869868be2014-05-15 10:33:05 -0400467
468 @Override
469 public void exit() {
470 Message message = obtainMessage(EVENT_PROVISIONING_NOTIFICATION, 0, 0, null);
471 message.replyTo = mNetworkAgentInfo.messenger;
472 mConnectivityServiceHandler.sendMessage(message);
473 mContext.unregisterReceiver(mUserRespondedBroadcastReceiver);
474 mUserRespondedBroadcastReceiver = null;
475 }
476 }
477
478 private class InteractiveAppsPromptedState extends State {
479 private AppRespondedBroadcastReceiver mReceiver;
480 @Override
481 public void enter() {
482 mReceiver = new AppRespondedBroadcastReceiver();
483 mReceiver.send(ConnectivityManager.ACTION_CAPTIVE_PORTAL_SIGN_IN);
484 }
485 @Override
486 public boolean processMessage(Message message) {
487 if (DBG) log(getName() + message.toString());
488 switch (message.what) {
489 case EVENT_APP_BYPASSED_CAPTIVE_PORTAL:
490 transitionTo(mValidatedState);
491 break;
492 case EVENT_APP_INDICATES_SIGN_IN_IMPOSSIBLE:
493 transitionTo(mOfflineState);
494 break;
495 case EVENT_NO_APP_RESPONSE:
496 transitionTo(mCaptivePortalState);
497 break;
498 default:
499 return NOT_HANDLED;
500 }
501 return HANDLED;
502 }
503 public void exit() {
504 mReceiver.cancel();
505 }
506 }
507
508 private class CaptivePortalState extends State {
509 private class CaptivePortalLoggedInBroadcastReceiver extends BroadcastReceiver {
510 private final int mToken;
511
512 CaptivePortalLoggedInBroadcastReceiver(int token) {
513 mToken = token;
514 }
515
516 @Override
517 public void onReceive(Context context, Intent intent) {
518 if (Integer.parseInt(intent.getStringExtra(Intent.EXTRA_TEXT)) ==
519 mNetworkAgentInfo.network.netId) {
520 sendMessage(obtainMessage(CMD_CAPTIVE_PORTAL_LOGGED_IN, mToken,
521 Integer.parseInt(intent.getStringExtra(LOGGED_IN_RESULT))));
522 }
523 }
524 }
525
526 private CaptivePortalLoggedInBroadcastReceiver mCaptivePortalLoggedInBroadcastReceiver;
527
528 @Override
529 public void enter() {
530 Intent intent = new Intent(Intent.ACTION_SEND);
531 intent.putExtra(Intent.EXTRA_TEXT, String.valueOf(mNetworkAgentInfo.network.netId));
532 intent.setType("text/plain");
533 intent.setComponent(new ComponentName("com.android.captiveportallogin",
534 "com.android.captiveportallogin.CaptivePortalLoginActivity"));
535 intent.setFlags(Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT | Intent.FLAG_ACTIVITY_NEW_TASK);
536
537 // Wait for result.
538 mCaptivePortalLoggedInBroadcastReceiver = new CaptivePortalLoggedInBroadcastReceiver(
539 ++mCaptivePortalLoggedInToken);
540 IntentFilter filter = new IntentFilter(ACTION_CAPTIVE_PORTAL_LOGGED_IN);
541 mContext.registerReceiver(mCaptivePortalLoggedInBroadcastReceiver, filter);
542 // Initiate app to log in.
543 mContext.startActivityAsUser(intent, UserHandle.CURRENT);
544 }
545
546 @Override
547 public boolean processMessage(Message message) {
548 if (DBG) log(getName() + message.toString());
549 switch (message.what) {
550 case CMD_CAPTIVE_PORTAL_LOGGED_IN:
551 if (message.arg1 != mCaptivePortalLoggedInToken)
552 break;
553 if (message.arg2 == 0) {
554 // TODO: Should teardown network.
555 transitionTo(mOfflineState);
556 } else {
557 transitionTo(mValidatedState);
558 }
559 break;
560 default:
561 return NOT_HANDLED;
562 }
563 return HANDLED;
564 }
565
566 @Override
567 public void exit() {
568 mContext.unregisterReceiver(mCaptivePortalLoggedInBroadcastReceiver);
569 mCaptivePortalLoggedInBroadcastReceiver = null;
570 }
Paul Jensenca8f16a2014-05-09 12:47:55 -0400571 }
572
573 private class LingeringState extends State {
574 @Override
575 public void enter() {
576 Message message = obtainMessage(CMD_LINGER_EXPIRED, ++mLingerToken, 0);
577 sendMessageDelayed(message, mLingerDelayMs);
578 }
579
580 @Override
581 public boolean processMessage(Message message) {
582 if (DBG) log(getName() + message.toString());
583 switch (message.what) {
584 case CMD_NETWORK_CONNECTED:
585 // Go straight to active as we've already evaluated.
586 transitionTo(mValidatedState);
587 break;
588 case CMD_LINGER_EXPIRED:
589 if (message.arg1 != mLingerToken)
590 break;
591 mConnectivityServiceHandler.sendMessage(
592 obtainMessage(EVENT_NETWORK_LINGER_COMPLETE, mNetworkAgentInfo));
593 break;
594 default:
595 return NOT_HANDLED;
596 }
597 return HANDLED;
598 }
599 }
600
601 /**
602 * Do a URL fetch on a known server to see if we get the data we expect.
603 * Returns HTTP response code.
604 */
605 private int isCaptivePortal() {
606 if (!mIsCaptivePortalCheckEnabled) return 204;
607
608 String urlString = "http://" + mServer + "/generate_204";
Paul Jensen869868be2014-05-15 10:33:05 -0400609 if (DBG) {
610 log("Checking " + urlString + " on " + mNetworkAgentInfo.networkInfo.getExtraInfo());
611 }
Paul Jensenca8f16a2014-05-09 12:47:55 -0400612 HttpURLConnection urlConnection = null;
613 Socket socket = null;
Paul Jensen869868be2014-05-15 10:33:05 -0400614 int httpResponseCode = 599;
Paul Jensenca8f16a2014-05-09 12:47:55 -0400615 try {
616 URL url = new URL(urlString);
617 if (false) {
618 // TODO: Need to add URLConnection.setNetwork() before we can enable.
619 urlConnection = (HttpURLConnection) url.openConnection();
620 urlConnection.setInstanceFollowRedirects(false);
621 urlConnection.setConnectTimeout(SOCKET_TIMEOUT_MS);
622 urlConnection.setReadTimeout(SOCKET_TIMEOUT_MS);
623 urlConnection.setUseCaches(false);
624 urlConnection.getInputStream();
625 httpResponseCode = urlConnection.getResponseCode();
626 } else {
Paul Jensen869868be2014-05-15 10:33:05 -0400627 socket = mNetworkAgentInfo.network.getSocketFactory().createSocket();
628 socket.setSoTimeout(SOCKET_TIMEOUT_MS);
629 // Lookup addresses only on this Network.
630 InetAddress[] hostAddresses = mNetworkAgentInfo.network.getAllByName(url.getHost());
631 // Try all addresses.
632 for (int i = 0; i < hostAddresses.length; i++) {
633 if (DBG) log("Connecting to " + hostAddresses[i]);
634 try {
635 socket.connect(new InetSocketAddress(hostAddresses[i],
636 url.getDefaultPort()), SOCKET_TIMEOUT_MS);
637 break;
638 } catch (IOException e) {
639 // Ignore exceptions on all but the last.
640 if (i == (hostAddresses.length - 1)) throw e;
641 }
642 }
643 if (DBG) log("Requesting " + url.getFile());
Paul Jensenca8f16a2014-05-09 12:47:55 -0400644 BufferedReader reader = new BufferedReader(
645 new InputStreamReader(socket.getInputStream()));
646 OutputStreamWriter writer = new OutputStreamWriter(socket.getOutputStream());
Paul Jensen869868be2014-05-15 10:33:05 -0400647 writer.write("GET " + url.getFile() + " HTTP/1.1\r\nHost: " + url.getHost() +
648 "\r\nConnection: close\r\n\r\n");
Paul Jensenca8f16a2014-05-09 12:47:55 -0400649 writer.flush();
650 String response = reader.readLine();
Paul Jensen869868be2014-05-15 10:33:05 -0400651 if (DBG) log("Received \"" + response + "\"");
652 if (response != null && (response.startsWith("HTTP/1.1 ") ||
653 // NOTE: We may want to consider an "HTTP/1.0 204" response to be a captive
654 // portal. The only example of this seen so far was a captive portal. For
655 // the time being go with prior behavior of assuming it's not a captive
656 // portal. If it is considered a captive portal, a different sign-in URL
657 // is needed (i.e. can't browse a 204). This could be the result of an HTTP
658 // proxy server.
659 response.startsWith("HTTP/1.0 "))) {
660 // NOTE: We may want to consider an "200" response with "Content-length=0" to
661 // not be a captive portal. This could be the result of an HTTP proxy server.
662 // See b/9972012.
Paul Jensenca8f16a2014-05-09 12:47:55 -0400663 httpResponseCode = Integer.parseInt(response.substring(9, 12));
Paul Jensen869868be2014-05-15 10:33:05 -0400664 } else {
665 // A response was received but not understood. The fact that a
666 // response was sent indicates there's some kind of responsive network
667 // out there so put up the notification to the user to log into the network
668 // so the user can have the final say as to whether the network is useful.
669 httpResponseCode = 399;
670 while (DBG && response != null && !response.isEmpty()) {
671 try {
672 response = reader.readLine();
673 } catch (IOException e) {
674 break;
675 }
676 log("Received \"" + response + "\"");
677 }
Paul Jensenca8f16a2014-05-09 12:47:55 -0400678 }
679 }
680 if (DBG) log("isCaptivePortal: ret=" + httpResponseCode);
681 } catch (IOException e) {
682 if (DBG) log("Probably not a portal: exception " + e);
Paul Jensen869868be2014-05-15 10:33:05 -0400683 if (httpResponseCode == 599) {
684 // TODO: Ping gateway and DNS server and log results.
685 }
Paul Jensenca8f16a2014-05-09 12:47:55 -0400686 } finally {
687 if (urlConnection != null) {
688 urlConnection.disconnect();
689 }
690 if (socket != null) {
691 try {
692 socket.close();
693 } catch (IOException e) {
694 // Ignore
695 }
696 }
697 }
698 return httpResponseCode;
699 }
700}