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