CaptivePortalTracker.java revision 9d4204d6257bf69885cf0718aa3adaa17457c318
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.content.BroadcastReceiver;
20import android.content.Context;
21import android.content.Intent;
22import android.content.IntentFilter;
23import android.database.ContentObserver;
24import android.net.ConnectivityManager;
25import android.net.IConnectivityManager;
26import android.net.wifi.WifiInfo;
27import android.net.wifi.WifiManager;
28import android.os.Handler;
29import android.os.Message;
30import android.os.RemoteException;
31import android.os.SystemClock;
32import android.provider.Settings;
33import android.telephony.CellIdentityCdma;
34import android.telephony.CellIdentityGsm;
35import android.telephony.CellIdentityLte;
36import android.telephony.CellIdentityWcdma;
37import android.telephony.CellInfo;
38import android.telephony.CellInfoCdma;
39import android.telephony.CellInfoGsm;
40import android.telephony.CellInfoLte;
41import android.telephony.CellInfoWcdma;
42import android.telephony.TelephonyManager;
43
44import com.android.internal.util.State;
45import com.android.internal.util.StateMachine;
46
47import java.io.IOException;
48import java.net.HttpURLConnection;
49import java.net.InetAddress;
50import java.net.Inet4Address;
51import java.net.SocketTimeoutException;
52import java.net.URL;
53import java.net.UnknownHostException;
54import java.util.List;
55
56/**
57 * This class allows captive portal detection on a network.
58 * @hide
59 */
60public class CaptivePortalTracker extends StateMachine {
61    private static final boolean DBG = true;
62    private static final String TAG = "CaptivePortalTracker";
63
64    private static final String DEFAULT_SERVER = "clients3.google.com";
65
66    private static final int SOCKET_TIMEOUT_MS = 10000;
67
68    public static final String ACTION_NETWORK_CONDITIONS_MEASURED =
69            "android.net.conn.NETWORK_CONDITIONS_MEASURED";
70    public static final String EXTRA_CONNECTIVITY_TYPE = "extra_connectivity_type";
71    public static final String EXTRA_NETWORK_TYPE = "extra_network_type";
72    public static final String EXTRA_RESPONSE_RECEIVED = "extra_response_received";
73    public static final String EXTRA_IS_CAPTIVE_PORTAL = "extra_is_captive_portal";
74    public static final String EXTRA_CELL_ID = "extra_cellid";
75    public static final String EXTRA_SSID = "extra_ssid";
76    public static final String EXTRA_BSSID = "extra_bssid";
77    /** real time since boot */
78    public static final String EXTRA_REQUEST_TIMESTAMP_MS = "extra_request_timestamp_ms";
79    public static final String EXTRA_RESPONSE_TIMESTAMP_MS = "extra_response_timestamp_ms";
80
81    private static final String PERMISSION_ACCESS_NETWORK_CONDITIONS =
82            "android.permission.ACCESS_NETWORK_CONDITIONS";
83
84    private String mServer;
85    private String mUrl;
86    private boolean mIsCaptivePortalCheckEnabled = false;
87    private IConnectivityManager mConnService;
88    private TelephonyManager mTelephonyManager;
89    private WifiManager mWifiManager;
90    private Context mContext;
91    private NetworkInfo mNetworkInfo;
92
93    private static final int CMD_DETECT_PORTAL          = 0;
94    private static final int CMD_CONNECTIVITY_CHANGE    = 1;
95    private static final int CMD_DELAYED_CAPTIVE_CHECK  = 2;
96
97    /* This delay happens every time before we do a captive check on a network */
98    private static final int DELAYED_CHECK_INTERVAL_MS = 10000;
99    private int mDelayedCheckToken = 0;
100
101    private State mDefaultState = new DefaultState();
102    private State mNoActiveNetworkState = new NoActiveNetworkState();
103    private State mActiveNetworkState = new ActiveNetworkState();
104    private State mDelayedCaptiveCheckState = new DelayedCaptiveCheckState();
105
106    private static final String SETUP_WIZARD_PACKAGE = "com.google.android.setupwizard";
107    private boolean mDeviceProvisioned = false;
108    private ProvisioningObserver mProvisioningObserver;
109
110    private CaptivePortalTracker(Context context, IConnectivityManager cs) {
111        super(TAG);
112
113        mContext = context;
114        mConnService = cs;
115        mTelephonyManager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
116        mWifiManager = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
117        mProvisioningObserver = new ProvisioningObserver();
118
119        IntentFilter filter = new IntentFilter();
120        filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION);
121        filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION_IMMEDIATE);
122        mContext.registerReceiver(mReceiver, filter);
123
124        mServer = Settings.Global.getString(mContext.getContentResolver(),
125                Settings.Global.CAPTIVE_PORTAL_SERVER);
126        if (mServer == null) mServer = DEFAULT_SERVER;
127
128        mIsCaptivePortalCheckEnabled = Settings.Global.getInt(mContext.getContentResolver(),
129                Settings.Global.CAPTIVE_PORTAL_DETECTION_ENABLED, 1) == 1;
130
131        addState(mDefaultState);
132            addState(mNoActiveNetworkState, mDefaultState);
133            addState(mActiveNetworkState, mDefaultState);
134                addState(mDelayedCaptiveCheckState, mActiveNetworkState);
135        setInitialState(mNoActiveNetworkState);
136    }
137
138    private class ProvisioningObserver extends ContentObserver {
139        ProvisioningObserver() {
140            super(new Handler());
141            mContext.getContentResolver().registerContentObserver(Settings.Global.getUriFor(
142                    Settings.Global.DEVICE_PROVISIONED), false, this);
143            onChange(false); // load initial value
144        }
145
146        @Override
147        public void onChange(boolean selfChange) {
148            mDeviceProvisioned = Settings.Global.getInt(mContext.getContentResolver(),
149                    Settings.Global.DEVICE_PROVISIONED, 0) != 0;
150        }
151    }
152
153    private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
154        @Override
155        public void onReceive(Context context, Intent intent) {
156            String action = intent.getAction();
157            // Normally, we respond to CONNECTIVITY_ACTION, allowing time for the change in
158            // connectivity to stabilize, but if the device is not yet provisioned, respond
159            // immediately to speed up transit through the setup wizard.
160            if ((mDeviceProvisioned && action.equals(ConnectivityManager.CONNECTIVITY_ACTION))
161                    || (!mDeviceProvisioned
162                            && action.equals(ConnectivityManager.CONNECTIVITY_ACTION_IMMEDIATE))) {
163                NetworkInfo info = intent.getParcelableExtra(
164                        ConnectivityManager.EXTRA_NETWORK_INFO);
165                sendMessage(obtainMessage(CMD_CONNECTIVITY_CHANGE, info));
166            }
167        }
168    };
169
170    public static CaptivePortalTracker makeCaptivePortalTracker(Context context,
171            IConnectivityManager cs) {
172        CaptivePortalTracker captivePortal = new CaptivePortalTracker(context, cs);
173        captivePortal.start();
174        return captivePortal;
175    }
176
177    public void detectCaptivePortal(NetworkInfo info) {
178        sendMessage(obtainMessage(CMD_DETECT_PORTAL, info));
179    }
180
181    private class DefaultState extends State {
182        @Override
183        public void enter() {
184            setNotificationOff();
185        }
186
187        @Override
188        public boolean processMessage(Message message) {
189            if (DBG) log(getName() + message.toString());
190            switch (message.what) {
191                case CMD_DETECT_PORTAL:
192                    NetworkInfo info = (NetworkInfo) message.obj;
193                    // Checking on a secondary connection is not supported
194                    // yet
195                    notifyPortalCheckComplete(info);
196                    break;
197                case CMD_CONNECTIVITY_CHANGE:
198                case CMD_DELAYED_CAPTIVE_CHECK:
199                    break;
200                default:
201                    loge("Ignoring " + message);
202                    break;
203            }
204            return HANDLED;
205        }
206    }
207
208    private class NoActiveNetworkState extends State {
209        @Override
210        public void enter() {
211            mNetworkInfo = null;
212        }
213
214        @Override
215        public boolean processMessage(Message message) {
216            if (DBG) log(getName() + message.toString());
217            InetAddress server;
218            NetworkInfo info;
219            switch (message.what) {
220                case CMD_CONNECTIVITY_CHANGE:
221                    info = (NetworkInfo) message.obj;
222                    if (info.getType() == ConnectivityManager.TYPE_WIFI) {
223                        if (info.isConnected() && isActiveNetwork(info)) {
224                            mNetworkInfo = info;
225                            transitionTo(mDelayedCaptiveCheckState);
226                        }
227                    } else {
228                        log(getName() + " not a wifi connectivity change, ignore");
229                    }
230                    break;
231                default:
232                    return NOT_HANDLED;
233            }
234            return HANDLED;
235        }
236    }
237
238    private class ActiveNetworkState extends State {
239        @Override
240        public void enter() {
241            setNotificationOff();
242        }
243
244        @Override
245        public boolean processMessage(Message message) {
246            NetworkInfo info;
247            switch (message.what) {
248               case CMD_CONNECTIVITY_CHANGE:
249                    info = (NetworkInfo) message.obj;
250                    if (!info.isConnected()
251                            && info.getType() == mNetworkInfo.getType()) {
252                        if (DBG) log("Disconnected from active network " + info);
253                        transitionTo(mNoActiveNetworkState);
254                    } else if (info.getType() != mNetworkInfo.getType() &&
255                            info.isConnected() &&
256                            isActiveNetwork(info)) {
257                        if (DBG) log("Active network switched " + info);
258                        deferMessage(message);
259                        transitionTo(mNoActiveNetworkState);
260                    }
261                    break;
262                default:
263                    return NOT_HANDLED;
264            }
265            return HANDLED;
266        }
267    }
268
269
270
271    private class DelayedCaptiveCheckState extends State {
272        @Override
273        public void enter() {
274            Message message = obtainMessage(CMD_DELAYED_CAPTIVE_CHECK, ++mDelayedCheckToken, 0);
275            if (mDeviceProvisioned) {
276                sendMessageDelayed(message, DELAYED_CHECK_INTERVAL_MS);
277            } else {
278                sendMessage(message);
279            }
280        }
281
282        @Override
283        public boolean processMessage(Message message) {
284            if (DBG) log(getName() + message.toString());
285            switch (message.what) {
286                case CMD_DELAYED_CAPTIVE_CHECK:
287                    if (message.arg1 == mDelayedCheckToken) {
288                        InetAddress server = lookupHost(mServer);
289                        boolean captive = server != null && isCaptivePortal(server);
290                        if (captive) {
291                            if (DBG) log("Captive network " + mNetworkInfo);
292                        } else {
293                            if (DBG) log("Not captive network " + mNetworkInfo);
294                        }
295                        notifyPortalCheckCompleted(mNetworkInfo, captive);
296                        if (mDeviceProvisioned) {
297                            if (captive) {
298                                // Setup Wizard will assist the user in connecting to a captive
299                                // portal, so make the notification visible unless during setup
300                                try {
301                                    mConnService.setProvisioningNotificationVisible(true,
302                                        mNetworkInfo.getType(), mNetworkInfo.getExtraInfo(), mUrl);
303                                } catch(RemoteException e) {
304                                    e.printStackTrace();
305                                }
306                            }
307                        } else {
308                            Intent intent = new Intent(
309                                    ConnectivityManager.ACTION_CAPTIVE_PORTAL_TEST_COMPLETED);
310                            intent.putExtra(ConnectivityManager.EXTRA_IS_CAPTIVE_PORTAL, captive);
311                            intent.setPackage(SETUP_WIZARD_PACKAGE);
312                            mContext.sendBroadcast(intent);
313                        }
314
315                        transitionTo(mActiveNetworkState);
316                    }
317                    break;
318                default:
319                    return NOT_HANDLED;
320            }
321            return HANDLED;
322        }
323    }
324
325    private void notifyPortalCheckComplete(NetworkInfo info) {
326        if (info == null) {
327            loge("notifyPortalCheckComplete on null");
328            return;
329        }
330        try {
331            if (DBG) log("notifyPortalCheckComplete: ni=" + info);
332            mConnService.captivePortalCheckComplete(info);
333        } catch(RemoteException e) {
334            e.printStackTrace();
335        }
336    }
337
338    private void notifyPortalCheckCompleted(NetworkInfo info, boolean isCaptivePortal) {
339        if (info == null) {
340            loge("notifyPortalCheckComplete on null");
341            return;
342        }
343        try {
344            if (DBG) log("notifyPortalCheckCompleted: captive=" + isCaptivePortal + " ni=" + info);
345            mConnService.captivePortalCheckCompleted(info, isCaptivePortal);
346        } catch(RemoteException e) {
347            e.printStackTrace();
348        }
349    }
350
351    private boolean isActiveNetwork(NetworkInfo info) {
352        try {
353            NetworkInfo active = mConnService.getActiveNetworkInfo();
354            if (active != null && active.getType() == info.getType()) {
355                return true;
356            }
357        } catch (RemoteException e) {
358            e.printStackTrace();
359        }
360        return false;
361    }
362
363    private void setNotificationOff() {
364        try {
365            mConnService.setProvisioningNotificationVisible(false, ConnectivityManager.TYPE_NONE,
366                    null, null);
367        } catch (RemoteException e) {
368            log("setNotificationOff: " + e);
369        }
370    }
371
372    /**
373     * Do a URL fetch on a known server to see if we get the data we expect.
374     * Measure the response time and broadcast that.
375     */
376    private boolean isCaptivePortal(InetAddress server) {
377        HttpURLConnection urlConnection = null;
378        if (!mIsCaptivePortalCheckEnabled) return false;
379
380        mUrl = "http://" + server.getHostAddress() + "/generate_204";
381        if (DBG) log("Checking " + mUrl);
382        long requestTimestamp = -1;
383        try {
384            URL url = new URL(mUrl);
385            urlConnection = (HttpURLConnection) url.openConnection();
386            urlConnection.setInstanceFollowRedirects(false);
387            urlConnection.setConnectTimeout(SOCKET_TIMEOUT_MS);
388            urlConnection.setReadTimeout(SOCKET_TIMEOUT_MS);
389            urlConnection.setUseCaches(false);
390
391            // Time how long it takes to get a response to our request
392            requestTimestamp = SystemClock.elapsedRealtime();
393
394            urlConnection.getInputStream();
395
396            // Time how long it takes to get a response to our request
397            long responseTimestamp = SystemClock.elapsedRealtime();
398
399            // we got a valid response, but not from the real google
400            int rspCode = urlConnection.getResponseCode();
401            boolean isCaptivePortal = rspCode != 204;
402
403            sendNetworkConditionsBroadcast(true /* response received */, isCaptivePortal,
404                    requestTimestamp, responseTimestamp);
405
406            if (DBG) log("isCaptivePortal: ret=" + isCaptivePortal + " rspCode=" + rspCode);
407            return isCaptivePortal;
408        } catch (IOException e) {
409            if (DBG) log("Probably not a portal: exception " + e);
410            if (requestTimestamp != -1) {
411                sendFailedCaptivePortalCheckBroadcast(requestTimestamp);
412            } // else something went wrong with setting up the urlConnection
413            return false;
414        } finally {
415            if (urlConnection != null) {
416                urlConnection.disconnect();
417            }
418        }
419    }
420
421    private InetAddress lookupHost(String hostname) {
422        InetAddress inetAddress[];
423        try {
424            inetAddress = InetAddress.getAllByName(hostname);
425        } catch (UnknownHostException e) {
426            sendFailedCaptivePortalCheckBroadcast(SystemClock.elapsedRealtime());
427            return null;
428        }
429
430        for (InetAddress a : inetAddress) {
431            if (a instanceof Inet4Address) return a;
432        }
433
434        sendFailedCaptivePortalCheckBroadcast(SystemClock.elapsedRealtime());
435        return null;
436    }
437
438    private void sendFailedCaptivePortalCheckBroadcast(long requestTimestampMs) {
439        sendNetworkConditionsBroadcast(false /* response received */, false /* ignored */,
440                requestTimestampMs, 0 /* ignored */);
441    }
442
443    /**
444     * @param responseReceived - whether or not we received a valid HTTP response to our request.
445     * If false, isCaptivePortal and responseTimestampMs are ignored
446     */
447    private void sendNetworkConditionsBroadcast(boolean responseReceived, boolean isCaptivePortal,
448            long requestTimestampMs, long responseTimestampMs) {
449        if (Settings.Global.getInt(mContext.getContentResolver(),
450                Settings.Global.WIFI_SCAN_ALWAYS_AVAILABLE, 0) == 0) {
451            if (DBG) log("Don't send network conditions - lacking user consent.");
452            return;
453        }
454
455        Intent latencyBroadcast = new Intent(ACTION_NETWORK_CONDITIONS_MEASURED);
456        switch (mNetworkInfo.getType()) {
457            case ConnectivityManager.TYPE_WIFI:
458                WifiInfo currentWifiInfo = mWifiManager.getConnectionInfo();
459                if (currentWifiInfo != null) {
460                    latencyBroadcast.putExtra(EXTRA_SSID, currentWifiInfo.getSSID());
461                    latencyBroadcast.putExtra(EXTRA_BSSID, currentWifiInfo.getBSSID());
462                } else {
463                    if (DBG) logw("network info is TYPE_WIFI but no ConnectionInfo found");
464                    return;
465                }
466                break;
467            case ConnectivityManager.TYPE_MOBILE:
468                latencyBroadcast.putExtra(EXTRA_NETWORK_TYPE, mTelephonyManager.getNetworkType());
469                List<CellInfo> info = mTelephonyManager.getAllCellInfo();
470                if (info == null) return;
471                StringBuffer uniqueCellId = new StringBuffer();
472                int numRegisteredCellInfo = 0;
473                for (CellInfo cellInfo : info) {
474                    if (cellInfo.isRegistered()) {
475                        numRegisteredCellInfo++;
476                        if (numRegisteredCellInfo > 1) {
477                            if (DBG) log("more than one registered CellInfo.  Can't " +
478                                    "tell which is active.  Bailing.");
479                            return;
480                        }
481                        if (cellInfo instanceof CellInfoCdma) {
482                            CellIdentityCdma cellId = ((CellInfoCdma) cellInfo).getCellIdentity();
483                            latencyBroadcast.putExtra(EXTRA_CELL_ID, cellId);
484                        } else if (cellInfo instanceof CellInfoGsm) {
485                            CellIdentityGsm cellId = ((CellInfoGsm) cellInfo).getCellIdentity();
486                            latencyBroadcast.putExtra(EXTRA_CELL_ID, cellId);
487                        } else if (cellInfo instanceof CellInfoLte) {
488                            CellIdentityLte cellId = ((CellInfoLte) cellInfo).getCellIdentity();
489                            latencyBroadcast.putExtra(EXTRA_CELL_ID, cellId);
490                        } else if (cellInfo instanceof CellInfoWcdma) {
491                            CellIdentityWcdma cellId = ((CellInfoWcdma) cellInfo).getCellIdentity();
492                            latencyBroadcast.putExtra(EXTRA_CELL_ID, cellId);
493                        } else {
494                            if (DBG) logw("Registered cellinfo is unrecognized");
495                            return;
496                        }
497                    }
498                }
499                break;
500            default:
501                return;
502        }
503        latencyBroadcast.putExtra(EXTRA_CONNECTIVITY_TYPE, mNetworkInfo.getType());
504        latencyBroadcast.putExtra(EXTRA_RESPONSE_RECEIVED, responseReceived);
505        latencyBroadcast.putExtra(EXTRA_REQUEST_TIMESTAMP_MS, requestTimestampMs);
506
507        if (responseReceived) {
508            latencyBroadcast.putExtra(EXTRA_IS_CAPTIVE_PORTAL, isCaptivePortal);
509            latencyBroadcast.putExtra(EXTRA_RESPONSE_TIMESTAMP_MS, responseTimestampMs);
510        }
511        mContext.sendBroadcast(latencyBroadcast, PERMISSION_ACCESS_NETWORK_CONDITIONS);
512    }
513}
514