blob: 5260185f1faa736a6d189157e7c8dbba6a7699b8 [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.
Paul Jensenfdc4e4a2014-07-15 12:07:36 -0400151 * arg2 = NetID.
152 * obj = Intent to be launched when notification selected by user, null if !arg1.
Paul Jensen869868be2014-05-15 10:33:05 -0400153 */
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:
Robert Greenwalt1fd9aee2014-07-17 16:11:38 -0700252 if (DBG) log("Disconnected - quitting");
253 quit();
Paul Jensenca8f16a2014-05-09 12:47:55 -0400254 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;
Paul Jensen6bc2c2c2014-05-07 15:27:40 -0400322 if (mNetworkAgentInfo.isVPN()) {
323 transitionTo(mValidatedState);
324 }
Paul Jensenca8f16a2014-05-09 12:47:55 -0400325 // If network provides no internet connectivity adjust evaluation.
Paul Jensen869868be2014-05-15 10:33:05 -0400326 if (!mNetworkAgentInfo.networkCapabilities.hasCapability(
Paul Jensenca8f16a2014-05-09 12:47:55 -0400327 NetworkCapabilities.NET_CAPABILITY_INTERNET)) {
328 // TODO: Try to verify something works. Do all gateways respond to pings?
329 transitionTo(mValidatedState);
330 }
331 // Kick off a thread to perform internet connectivity evaluation.
332 Thread thread = new EvaluateInternetConnectivity(mReevaluateToken);
333 thread.run();
334 break;
335 case EVENT_REEVALUATION_COMPLETE:
336 if (message.arg1 != mReevaluateToken)
337 break;
338 int httpResponseCode = message.arg2;
339 if (httpResponseCode == 204) {
340 transitionTo(mValidatedState);
341 } else if (httpResponseCode >= 200 && httpResponseCode <= 399) {
Paul Jensen869868be2014-05-15 10:33:05 -0400342 transitionTo(mUninteractiveAppsPromptedState);
343 } else if (++mRetries > MAX_RETRIES) {
344 transitionTo(mOfflineState);
345 } else if (mReevaluateDelayMs >= 0) {
346 Message msg = obtainMessage(CMD_REEVALUATE, ++mReevaluateToken, 0);
347 sendMessageDelayed(msg, mReevaluateDelayMs);
Paul Jensenca8f16a2014-05-09 12:47:55 -0400348 }
349 break;
350 default:
351 return NOT_HANDLED;
352 }
353 return HANDLED;
354 }
355 }
356
Paul Jensen869868be2014-05-15 10:33:05 -0400357 private class AppRespondedBroadcastReceiver extends BroadcastReceiver {
358 private static final int CAPTIVE_PORTAL_UNINITIALIZED_RETURN_CODE = 0;
359 private boolean mCanceled;
360 AppRespondedBroadcastReceiver() {
361 mCanceled = false;
362 }
363 public void send(String action) {
364 Intent intent = new Intent(action);
365 intent.putExtra(ConnectivityManager.EXTRA_NETWORK, mNetworkAgentInfo.network);
366 mContext.sendOrderedBroadcastAsUser(intent, UserHandle.ALL, null, this, getHandler(),
367 CAPTIVE_PORTAL_UNINITIALIZED_RETURN_CODE, null, null);
368 }
369 public void cancel() {
370 mCanceled = true;
371 }
372 @Override
373 public void onReceive(Context context, Intent intent) {
374 if (!mCanceled) {
375 cancel();
376 switch (getResultCode()) {
377 case ConnectivityManager.CAPTIVE_PORTAL_SIGNED_IN:
378 sendMessage(EVENT_APP_BYPASSED_CAPTIVE_PORTAL);
379 break;
380 case ConnectivityManager.CAPTIVE_PORTAL_DISCONNECT:
381 sendMessage(EVENT_APP_INDICATES_SIGN_IN_IMPOSSIBLE);
382 break;
383 // NOTE: This case label makes compiler enforce no overlap between result codes.
384 case CAPTIVE_PORTAL_UNINITIALIZED_RETURN_CODE:
385 default:
386 sendMessage(EVENT_NO_APP_RESPONSE);
387 break;
388 }
389 }
390 }
391 }
392
393 private class UninteractiveAppsPromptedState extends State {
394 private AppRespondedBroadcastReceiver mReceiver;
Paul Jensenca8f16a2014-05-09 12:47:55 -0400395 @Override
396 public void enter() {
Paul Jensen869868be2014-05-15 10:33:05 -0400397 mReceiver = new AppRespondedBroadcastReceiver();
398 mReceiver.send(ConnectivityManager.ACTION_CAPTIVE_PORTAL_DETECTED);
399 }
400 @Override
401 public boolean processMessage(Message message) {
402 if (DBG) log(getName() + message.toString());
403 switch (message.what) {
404 case EVENT_APP_BYPASSED_CAPTIVE_PORTAL:
405 transitionTo(mValidatedState);
406 break;
407 case EVENT_APP_INDICATES_SIGN_IN_IMPOSSIBLE:
408 transitionTo(mOfflineState);
409 break;
410 case EVENT_NO_APP_RESPONSE:
411 transitionTo(mUserPromptedState);
412 break;
413 default:
414 return NOT_HANDLED;
415 }
416 return HANDLED;
417 }
418 public void exit() {
419 mReceiver.cancel();
420 }
421 }
422
423 private class UserPromptedState extends State {
424 private class UserRespondedBroadcastReceiver extends BroadcastReceiver {
425 private final int mToken;
426 UserRespondedBroadcastReceiver(int token) {
427 mToken = token;
428 }
429 @Override
430 public void onReceive(Context context, Intent intent) {
431 if (Integer.parseInt(intent.getStringExtra(Intent.EXTRA_TEXT)) ==
432 mNetworkAgentInfo.network.netId) {
433 sendMessage(obtainMessage(CMD_USER_WANTS_SIGN_IN, mToken));
434 }
435 }
436 }
437
438 private UserRespondedBroadcastReceiver mUserRespondedBroadcastReceiver;
439
440 @Override
441 public void enter() {
442 // Wait for user to select sign-in notifcation.
443 mUserRespondedBroadcastReceiver = new UserRespondedBroadcastReceiver(
444 ++mUserPromptedToken);
445 IntentFilter filter = new IntentFilter(ACTION_SIGN_IN_REQUESTED);
446 mContext.registerReceiver(mUserRespondedBroadcastReceiver, filter);
447 // Initiate notification to sign-in.
448 Intent intent = new Intent(ACTION_SIGN_IN_REQUESTED);
449 intent.putExtra(Intent.EXTRA_TEXT, String.valueOf(mNetworkAgentInfo.network.netId));
Paul Jensenfdc4e4a2014-07-15 12:07:36 -0400450 Message message = obtainMessage(EVENT_PROVISIONING_NOTIFICATION, 1,
451 mNetworkAgentInfo.network.netId,
Paul Jensen869868be2014-05-15 10:33:05 -0400452 PendingIntent.getBroadcast(mContext, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT));
Paul Jensen869868be2014-05-15 10:33:05 -0400453 mConnectivityServiceHandler.sendMessage(message);
Paul Jensenca8f16a2014-05-09 12:47:55 -0400454 }
455
456 @Override
457 public boolean processMessage(Message message) {
458 if (DBG) log(getName() + message.toString());
459 switch (message.what) {
Paul Jensen869868be2014-05-15 10:33:05 -0400460 case CMD_USER_WANTS_SIGN_IN:
461 if (message.arg1 != mUserPromptedToken)
Paul Jensenca8f16a2014-05-09 12:47:55 -0400462 break;
Paul Jensen869868be2014-05-15 10:33:05 -0400463 transitionTo(mInteractiveAppsPromptedState);
Paul Jensenca8f16a2014-05-09 12:47:55 -0400464 break;
465 default:
466 return NOT_HANDLED;
467 }
468 return HANDLED;
469 }
Paul Jensen869868be2014-05-15 10:33:05 -0400470
471 @Override
472 public void exit() {
Paul Jensenfdc4e4a2014-07-15 12:07:36 -0400473 Message message = obtainMessage(EVENT_PROVISIONING_NOTIFICATION, 0,
474 mNetworkAgentInfo.network.netId, null);
Paul Jensen869868be2014-05-15 10:33:05 -0400475 mConnectivityServiceHandler.sendMessage(message);
476 mContext.unregisterReceiver(mUserRespondedBroadcastReceiver);
477 mUserRespondedBroadcastReceiver = null;
478 }
479 }
480
481 private class InteractiveAppsPromptedState extends State {
482 private AppRespondedBroadcastReceiver mReceiver;
483 @Override
484 public void enter() {
485 mReceiver = new AppRespondedBroadcastReceiver();
486 mReceiver.send(ConnectivityManager.ACTION_CAPTIVE_PORTAL_SIGN_IN);
487 }
488 @Override
489 public boolean processMessage(Message message) {
490 if (DBG) log(getName() + message.toString());
491 switch (message.what) {
492 case EVENT_APP_BYPASSED_CAPTIVE_PORTAL:
493 transitionTo(mValidatedState);
494 break;
495 case EVENT_APP_INDICATES_SIGN_IN_IMPOSSIBLE:
496 transitionTo(mOfflineState);
497 break;
498 case EVENT_NO_APP_RESPONSE:
499 transitionTo(mCaptivePortalState);
500 break;
501 default:
502 return NOT_HANDLED;
503 }
504 return HANDLED;
505 }
506 public void exit() {
507 mReceiver.cancel();
508 }
509 }
510
511 private class CaptivePortalState extends State {
512 private class CaptivePortalLoggedInBroadcastReceiver extends BroadcastReceiver {
513 private final int mToken;
514
515 CaptivePortalLoggedInBroadcastReceiver(int token) {
516 mToken = token;
517 }
518
519 @Override
520 public void onReceive(Context context, Intent intent) {
521 if (Integer.parseInt(intent.getStringExtra(Intent.EXTRA_TEXT)) ==
522 mNetworkAgentInfo.network.netId) {
523 sendMessage(obtainMessage(CMD_CAPTIVE_PORTAL_LOGGED_IN, mToken,
524 Integer.parseInt(intent.getStringExtra(LOGGED_IN_RESULT))));
525 }
526 }
527 }
528
529 private CaptivePortalLoggedInBroadcastReceiver mCaptivePortalLoggedInBroadcastReceiver;
530
531 @Override
532 public void enter() {
533 Intent intent = new Intent(Intent.ACTION_SEND);
534 intent.putExtra(Intent.EXTRA_TEXT, String.valueOf(mNetworkAgentInfo.network.netId));
535 intent.setType("text/plain");
536 intent.setComponent(new ComponentName("com.android.captiveportallogin",
537 "com.android.captiveportallogin.CaptivePortalLoginActivity"));
538 intent.setFlags(Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT | Intent.FLAG_ACTIVITY_NEW_TASK);
539
540 // Wait for result.
541 mCaptivePortalLoggedInBroadcastReceiver = new CaptivePortalLoggedInBroadcastReceiver(
542 ++mCaptivePortalLoggedInToken);
543 IntentFilter filter = new IntentFilter(ACTION_CAPTIVE_PORTAL_LOGGED_IN);
544 mContext.registerReceiver(mCaptivePortalLoggedInBroadcastReceiver, filter);
545 // Initiate app to log in.
546 mContext.startActivityAsUser(intent, UserHandle.CURRENT);
547 }
548
549 @Override
550 public boolean processMessage(Message message) {
551 if (DBG) log(getName() + message.toString());
552 switch (message.what) {
553 case CMD_CAPTIVE_PORTAL_LOGGED_IN:
554 if (message.arg1 != mCaptivePortalLoggedInToken)
555 break;
556 if (message.arg2 == 0) {
557 // TODO: Should teardown network.
558 transitionTo(mOfflineState);
559 } else {
560 transitionTo(mValidatedState);
561 }
562 break;
563 default:
564 return NOT_HANDLED;
565 }
566 return HANDLED;
567 }
568
569 @Override
570 public void exit() {
571 mContext.unregisterReceiver(mCaptivePortalLoggedInBroadcastReceiver);
572 mCaptivePortalLoggedInBroadcastReceiver = null;
573 }
Paul Jensenca8f16a2014-05-09 12:47:55 -0400574 }
575
576 private class LingeringState extends State {
577 @Override
578 public void enter() {
579 Message message = obtainMessage(CMD_LINGER_EXPIRED, ++mLingerToken, 0);
580 sendMessageDelayed(message, mLingerDelayMs);
581 }
582
583 @Override
584 public boolean processMessage(Message message) {
585 if (DBG) log(getName() + message.toString());
586 switch (message.what) {
587 case CMD_NETWORK_CONNECTED:
588 // Go straight to active as we've already evaluated.
589 transitionTo(mValidatedState);
590 break;
591 case CMD_LINGER_EXPIRED:
592 if (message.arg1 != mLingerToken)
593 break;
594 mConnectivityServiceHandler.sendMessage(
595 obtainMessage(EVENT_NETWORK_LINGER_COMPLETE, mNetworkAgentInfo));
596 break;
597 default:
598 return NOT_HANDLED;
599 }
600 return HANDLED;
601 }
602 }
603
604 /**
605 * Do a URL fetch on a known server to see if we get the data we expect.
606 * Returns HTTP response code.
607 */
608 private int isCaptivePortal() {
609 if (!mIsCaptivePortalCheckEnabled) return 204;
610
611 String urlString = "http://" + mServer + "/generate_204";
Paul Jensen869868be2014-05-15 10:33:05 -0400612 if (DBG) {
613 log("Checking " + urlString + " on " + mNetworkAgentInfo.networkInfo.getExtraInfo());
614 }
Paul Jensenca8f16a2014-05-09 12:47:55 -0400615 HttpURLConnection urlConnection = null;
616 Socket socket = null;
Paul Jensen869868be2014-05-15 10:33:05 -0400617 int httpResponseCode = 599;
Paul Jensenca8f16a2014-05-09 12:47:55 -0400618 try {
619 URL url = new URL(urlString);
620 if (false) {
621 // TODO: Need to add URLConnection.setNetwork() before we can enable.
622 urlConnection = (HttpURLConnection) url.openConnection();
623 urlConnection.setInstanceFollowRedirects(false);
624 urlConnection.setConnectTimeout(SOCKET_TIMEOUT_MS);
625 urlConnection.setReadTimeout(SOCKET_TIMEOUT_MS);
626 urlConnection.setUseCaches(false);
627 urlConnection.getInputStream();
628 httpResponseCode = urlConnection.getResponseCode();
629 } else {
Paul Jensen869868be2014-05-15 10:33:05 -0400630 socket = mNetworkAgentInfo.network.getSocketFactory().createSocket();
631 socket.setSoTimeout(SOCKET_TIMEOUT_MS);
632 // Lookup addresses only on this Network.
633 InetAddress[] hostAddresses = mNetworkAgentInfo.network.getAllByName(url.getHost());
634 // Try all addresses.
635 for (int i = 0; i < hostAddresses.length; i++) {
636 if (DBG) log("Connecting to " + hostAddresses[i]);
637 try {
638 socket.connect(new InetSocketAddress(hostAddresses[i],
639 url.getDefaultPort()), SOCKET_TIMEOUT_MS);
640 break;
641 } catch (IOException e) {
642 // Ignore exceptions on all but the last.
643 if (i == (hostAddresses.length - 1)) throw e;
644 }
645 }
646 if (DBG) log("Requesting " + url.getFile());
Paul Jensenca8f16a2014-05-09 12:47:55 -0400647 BufferedReader reader = new BufferedReader(
648 new InputStreamReader(socket.getInputStream()));
649 OutputStreamWriter writer = new OutputStreamWriter(socket.getOutputStream());
Paul Jensen869868be2014-05-15 10:33:05 -0400650 writer.write("GET " + url.getFile() + " HTTP/1.1\r\nHost: " + url.getHost() +
Paul Jensen2bb7e342014-07-15 10:57:38 -0400651 "\r\nUser-Agent: " + System.getProperty("http.agent") +
Paul Jensen869868be2014-05-15 10:33:05 -0400652 "\r\nConnection: close\r\n\r\n");
Paul Jensenca8f16a2014-05-09 12:47:55 -0400653 writer.flush();
654 String response = reader.readLine();
Paul Jensen869868be2014-05-15 10:33:05 -0400655 if (DBG) log("Received \"" + response + "\"");
656 if (response != null && (response.startsWith("HTTP/1.1 ") ||
657 // NOTE: We may want to consider an "HTTP/1.0 204" response to be a captive
658 // portal. The only example of this seen so far was a captive portal. For
659 // the time being go with prior behavior of assuming it's not a captive
660 // portal. If it is considered a captive portal, a different sign-in URL
661 // is needed (i.e. can't browse a 204). This could be the result of an HTTP
662 // proxy server.
663 response.startsWith("HTTP/1.0 "))) {
664 // NOTE: We may want to consider an "200" response with "Content-length=0" to
665 // not be a captive portal. This could be the result of an HTTP proxy server.
666 // See b/9972012.
Paul Jensenca8f16a2014-05-09 12:47:55 -0400667 httpResponseCode = Integer.parseInt(response.substring(9, 12));
Paul Jensen869868be2014-05-15 10:33:05 -0400668 } else {
669 // A response was received but not understood. The fact that a
670 // response was sent indicates there's some kind of responsive network
671 // out there so put up the notification to the user to log into the network
672 // so the user can have the final say as to whether the network is useful.
673 httpResponseCode = 399;
674 while (DBG && response != null && !response.isEmpty()) {
675 try {
676 response = reader.readLine();
677 } catch (IOException e) {
678 break;
679 }
680 log("Received \"" + response + "\"");
681 }
Paul Jensenca8f16a2014-05-09 12:47:55 -0400682 }
683 }
684 if (DBG) log("isCaptivePortal: ret=" + httpResponseCode);
685 } catch (IOException e) {
686 if (DBG) log("Probably not a portal: exception " + e);
Paul Jensen869868be2014-05-15 10:33:05 -0400687 if (httpResponseCode == 599) {
688 // TODO: Ping gateway and DNS server and log results.
689 }
Paul Jensenca8f16a2014-05-09 12:47:55 -0400690 } finally {
691 if (urlConnection != null) {
692 urlConnection.disconnect();
693 }
694 if (socket != null) {
695 try {
696 socket.close();
697 } catch (IOException e) {
698 // Ignore
699 }
700 }
701 }
702 return httpResponseCode;
703 }
704}