CaptivePortalTracker.java revision 948282b0e6cf5310f09db97a4ae939db7c1cef72
1/*
2 * Copyright (C) 2012 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 android.net;
18
19import android.app.Activity;
20import android.app.Notification;
21import android.app.NotificationManager;
22import android.app.PendingIntent;
23import android.content.BroadcastReceiver;
24import android.content.Context;
25import android.content.Intent;
26import android.content.IntentFilter;
27import android.content.res.Resources;
28import android.database.ContentObserver;
29import android.net.ConnectivityManager;
30import android.net.IConnectivityManager;
31import android.os.Handler;
32import android.os.UserHandle;
33import android.os.Message;
34import android.os.RemoteException;
35import android.provider.Settings;
36import android.telephony.TelephonyManager;
37import android.text.TextUtils;
38
39import com.android.internal.util.State;
40import com.android.internal.util.StateMachine;
41
42import java.io.IOException;
43import java.net.HttpURLConnection;
44import java.net.InetAddress;
45import java.net.Inet4Address;
46import java.net.SocketTimeoutException;
47import java.net.URL;
48import java.net.UnknownHostException;
49
50import com.android.internal.R;
51
52/**
53 * This class allows captive portal detection on a network.
54 * @hide
55 */
56public class CaptivePortalTracker extends StateMachine {
57    private static final boolean DBG = true;
58    private static final String TAG = "CaptivePortalTracker";
59
60    private static final String DEFAULT_SERVER = "clients3.google.com";
61
62    private static final int SOCKET_TIMEOUT_MS = 10000;
63
64    private String mServer;
65    private String mUrl;
66    private boolean mIsCaptivePortalCheckEnabled = false;
67    private IConnectivityManager mConnService;
68    private TelephonyManager mTelephonyManager;
69    private Context mContext;
70    private NetworkInfo mNetworkInfo;
71
72    private static final int CMD_DETECT_PORTAL          = 0;
73    private static final int CMD_CONNECTIVITY_CHANGE    = 1;
74    private static final int CMD_DELAYED_CAPTIVE_CHECK  = 2;
75
76    /* This delay happens every time before we do a captive check on a network */
77    private static final int DELAYED_CHECK_INTERVAL_MS = 10000;
78    private int mDelayedCheckToken = 0;
79
80    private State mDefaultState = new DefaultState();
81    private State mNoActiveNetworkState = new NoActiveNetworkState();
82    private State mActiveNetworkState = new ActiveNetworkState();
83    private State mDelayedCaptiveCheckState = new DelayedCaptiveCheckState();
84
85    private static final String SETUP_WIZARD_PACKAGE = "com.google.android.setupwizard";
86    private boolean mDeviceProvisioned = false;
87    private ProvisioningObserver mProvisioningObserver;
88
89    private CaptivePortalTracker(Context context, IConnectivityManager cs) {
90        super(TAG);
91
92        mContext = context;
93        mConnService = cs;
94        mTelephonyManager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
95        mProvisioningObserver = new ProvisioningObserver();
96
97        IntentFilter filter = new IntentFilter();
98        filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION);
99        filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION_IMMEDIATE);
100        mContext.registerReceiver(mReceiver, filter);
101
102        mServer = Settings.Global.getString(mContext.getContentResolver(),
103                Settings.Global.CAPTIVE_PORTAL_SERVER);
104        if (mServer == null) mServer = DEFAULT_SERVER;
105
106        mIsCaptivePortalCheckEnabled = Settings.Global.getInt(mContext.getContentResolver(),
107                Settings.Global.CAPTIVE_PORTAL_DETECTION_ENABLED, 1) == 1;
108
109        addState(mDefaultState);
110            addState(mNoActiveNetworkState, mDefaultState);
111            addState(mActiveNetworkState, mDefaultState);
112                addState(mDelayedCaptiveCheckState, mActiveNetworkState);
113        setInitialState(mNoActiveNetworkState);
114    }
115
116    private class ProvisioningObserver extends ContentObserver {
117        ProvisioningObserver() {
118            super(new Handler());
119            mContext.getContentResolver().registerContentObserver(Settings.Global.getUriFor(
120                    Settings.Global.DEVICE_PROVISIONED), false, this);
121            onChange(false); // load initial value
122        }
123
124        @Override
125        public void onChange(boolean selfChange) {
126            mDeviceProvisioned = Settings.Global.getInt(mContext.getContentResolver(),
127                    Settings.Global.DEVICE_PROVISIONED, 0) != 0;
128        }
129    }
130
131    private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
132        @Override
133        public void onReceive(Context context, Intent intent) {
134            String action = intent.getAction();
135            // Normally, we respond to CONNECTIVITY_ACTION, allowing time for the change in
136            // connectivity to stabilize, but if the device is not yet provisioned, respond
137            // immediately to speed up transit through the setup wizard.
138            if ((mDeviceProvisioned && action.equals(ConnectivityManager.CONNECTIVITY_ACTION))
139                    || (!mDeviceProvisioned
140                            && action.equals(ConnectivityManager.CONNECTIVITY_ACTION_IMMEDIATE))) {
141                NetworkInfo info = intent.getParcelableExtra(
142                        ConnectivityManager.EXTRA_NETWORK_INFO);
143                sendMessage(obtainMessage(CMD_CONNECTIVITY_CHANGE, info));
144            }
145        }
146    };
147
148    public static CaptivePortalTracker makeCaptivePortalTracker(Context context,
149            IConnectivityManager cs) {
150        CaptivePortalTracker captivePortal = new CaptivePortalTracker(context, cs);
151        captivePortal.start();
152        return captivePortal;
153    }
154
155    public void detectCaptivePortal(NetworkInfo info) {
156        sendMessage(obtainMessage(CMD_DETECT_PORTAL, info));
157    }
158
159    private class DefaultState extends State {
160        @Override
161        public void enter() {
162            setNotificationOff();
163        }
164
165        @Override
166        public boolean processMessage(Message message) {
167            if (DBG) log(getName() + message.toString());
168            switch (message.what) {
169                case CMD_DETECT_PORTAL:
170                    NetworkInfo info = (NetworkInfo) message.obj;
171                    // Checking on a secondary connection is not supported
172                    // yet
173                    notifyPortalCheckComplete(info);
174                    break;
175                case CMD_CONNECTIVITY_CHANGE:
176                case CMD_DELAYED_CAPTIVE_CHECK:
177                    break;
178                default:
179                    loge("Ignoring " + message);
180                    break;
181            }
182            return HANDLED;
183        }
184    }
185
186    private class NoActiveNetworkState extends State {
187        @Override
188        public void enter() {
189            mNetworkInfo = null;
190        }
191
192        @Override
193        public boolean processMessage(Message message) {
194            if (DBG) log(getName() + message.toString());
195            InetAddress server;
196            NetworkInfo info;
197            switch (message.what) {
198                case CMD_CONNECTIVITY_CHANGE:
199                    info = (NetworkInfo) message.obj;
200                    if (info.getType() == ConnectivityManager.TYPE_WIFI) {
201                        if (info.isConnected() && isActiveNetwork(info)) {
202                            mNetworkInfo = info;
203                            transitionTo(mDelayedCaptiveCheckState);
204                        }
205                    } else {
206                        log(getName() + " not a wifi connectivity change, ignore");
207                    }
208                    break;
209                default:
210                    return NOT_HANDLED;
211            }
212            return HANDLED;
213        }
214    }
215
216    private class ActiveNetworkState extends State {
217        @Override
218        public void enter() {
219            setNotificationOff();
220        }
221
222        @Override
223        public boolean processMessage(Message message) {
224            NetworkInfo info;
225            switch (message.what) {
226               case CMD_CONNECTIVITY_CHANGE:
227                    info = (NetworkInfo) message.obj;
228                    if (!info.isConnected()
229                            && info.getType() == mNetworkInfo.getType()) {
230                        if (DBG) log("Disconnected from active network " + info);
231                        transitionTo(mNoActiveNetworkState);
232                    } else if (info.getType() != mNetworkInfo.getType() &&
233                            info.isConnected() &&
234                            isActiveNetwork(info)) {
235                        if (DBG) log("Active network switched " + info);
236                        deferMessage(message);
237                        transitionTo(mNoActiveNetworkState);
238                    }
239                    break;
240                default:
241                    return NOT_HANDLED;
242            }
243            return HANDLED;
244        }
245    }
246
247
248
249    private class DelayedCaptiveCheckState extends State {
250        @Override
251        public void enter() {
252            Message message = obtainMessage(CMD_DELAYED_CAPTIVE_CHECK, ++mDelayedCheckToken, 0);
253            if (mDeviceProvisioned) {
254                sendMessageDelayed(message, DELAYED_CHECK_INTERVAL_MS);
255            } else {
256                sendMessage(message);
257            }
258        }
259
260        @Override
261        public boolean processMessage(Message message) {
262            if (DBG) log(getName() + message.toString());
263            switch (message.what) {
264                case CMD_DELAYED_CAPTIVE_CHECK:
265                    if (message.arg1 == mDelayedCheckToken) {
266                        InetAddress server = lookupHost(mServer);
267                        boolean captive = server != null && isCaptivePortal(server);
268                        if (captive) {
269                            if (DBG) log("Captive network " + mNetworkInfo);
270                        } else {
271                            if (DBG) log("Not captive network " + mNetworkInfo);
272                        }
273                        notifyPortalCheckCompleted(mNetworkInfo, captive);
274                        if (mDeviceProvisioned) {
275                            if (captive) {
276                                // Setup Wizard will assist the user in connecting to a captive
277                                // portal, so make the notification visible unless during setup
278                                try {
279                                    mConnService.setProvisioningNotificationVisible(true,
280                                        mNetworkInfo.getType(), mNetworkInfo.getExtraInfo(), mUrl);
281                                } catch(RemoteException e) {
282                                    e.printStackTrace();
283                                }
284                            }
285                        } else {
286                            Intent intent = new Intent(
287                                    ConnectivityManager.ACTION_CAPTIVE_PORTAL_TEST_COMPLETED);
288                            intent.putExtra(ConnectivityManager.EXTRA_IS_CAPTIVE_PORTAL, captive);
289                            intent.setPackage(SETUP_WIZARD_PACKAGE);
290                            mContext.sendBroadcast(intent);
291                        }
292
293                        transitionTo(mActiveNetworkState);
294                    }
295                    break;
296                default:
297                    return NOT_HANDLED;
298            }
299            return HANDLED;
300        }
301    }
302
303    private void notifyPortalCheckComplete(NetworkInfo info) {
304        if (info == null) {
305            loge("notifyPortalCheckComplete on null");
306            return;
307        }
308        try {
309            if (DBG) log("notifyPortalCheckComplete: ni=" + info);
310            mConnService.captivePortalCheckComplete(info);
311        } catch(RemoteException e) {
312            e.printStackTrace();
313        }
314    }
315
316    private void notifyPortalCheckCompleted(NetworkInfo info, boolean isCaptivePortal) {
317        if (info == null) {
318            loge("notifyPortalCheckComplete on null");
319            return;
320        }
321        try {
322            if (DBG) log("notifyPortalCheckCompleted: captive=" + isCaptivePortal + " ni=" + info);
323            mConnService.captivePortalCheckCompleted(info, isCaptivePortal);
324        } catch(RemoteException e) {
325            e.printStackTrace();
326        }
327    }
328
329    private boolean isActiveNetwork(NetworkInfo info) {
330        try {
331            NetworkInfo active = mConnService.getActiveNetworkInfo();
332            if (active != null && active.getType() == info.getType()) {
333                return true;
334            }
335        } catch (RemoteException e) {
336            e.printStackTrace();
337        }
338        return false;
339    }
340
341    private void setNotificationOff() {
342        try {
343            mConnService.setProvisioningNotificationVisible(false, ConnectivityManager.TYPE_NONE,
344                    null, null);
345        } catch (RemoteException e) {
346            log("setNotificationOff: " + e);
347        }
348    }
349
350    /**
351     * Do a URL fetch on a known server to see if we get the data we expect
352     */
353    private boolean isCaptivePortal(InetAddress server) {
354        HttpURLConnection urlConnection = null;
355        if (!mIsCaptivePortalCheckEnabled) return false;
356
357        mUrl = "http://" + server.getHostAddress() + "/generate_204";
358        if (DBG) log("Checking " + mUrl);
359        try {
360            URL url = new URL(mUrl);
361            urlConnection = (HttpURLConnection) url.openConnection();
362            urlConnection.setInstanceFollowRedirects(false);
363            urlConnection.setConnectTimeout(SOCKET_TIMEOUT_MS);
364            urlConnection.setReadTimeout(SOCKET_TIMEOUT_MS);
365            urlConnection.setUseCaches(false);
366            urlConnection.getInputStream();
367            // we got a valid response, but not from the real google
368            return urlConnection.getResponseCode() != 204;
369        } catch (IOException e) {
370            if (DBG) log("Probably not a portal: exception " + e);
371            return false;
372        } finally {
373            if (urlConnection != null) {
374                urlConnection.disconnect();
375            }
376        }
377    }
378
379    private InetAddress lookupHost(String hostname) {
380        InetAddress inetAddress[];
381        try {
382            inetAddress = InetAddress.getAllByName(hostname);
383        } catch (UnknownHostException e) {
384            return null;
385        }
386
387        for (InetAddress a : inetAddress) {
388            if (a instanceof Inet4Address) return a;
389        }
390        return null;
391    }
392}
393