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
183        @Override
184        public boolean processMessage(Message message) {
185            if (DBG) log(getName() + message.toString());
186            switch (message.what) {
187                case CMD_DETECT_PORTAL:
188                    NetworkInfo info = (NetworkInfo) message.obj;
189                    // Checking on a secondary connection is not supported
190                    // yet
191                    notifyPortalCheckComplete(info);
192                    break;
193                case CMD_CONNECTIVITY_CHANGE:
194                case CMD_DELAYED_CAPTIVE_CHECK:
195                    break;
196                default:
197                    loge("Ignoring " + message);
198                    break;
199            }
200            return HANDLED;
201        }
202    }
203
204    private class NoActiveNetworkState extends State {
205        @Override
206        public void enter() {
207            setNotificationOff();
208            mNetworkInfo = null;
209        }
210
211        @Override
212        public boolean processMessage(Message message) {
213            if (DBG) log(getName() + message.toString());
214            InetAddress server;
215            NetworkInfo info;
216            switch (message.what) {
217                case CMD_CONNECTIVITY_CHANGE:
218                    info = (NetworkInfo) message.obj;
219                    if (info.getType() == ConnectivityManager.TYPE_WIFI) {
220                        if (info.isConnected() && isActiveNetwork(info)) {
221                            mNetworkInfo = info;
222                            transitionTo(mDelayedCaptiveCheckState);
223                        }
224                    } else {
225                        log(getName() + " not a wifi connectivity change, ignore");
226                    }
227                    break;
228                default:
229                    return NOT_HANDLED;
230            }
231            return HANDLED;
232        }
233    }
234
235    private class ActiveNetworkState extends State {
236        @Override
237        public boolean processMessage(Message message) {
238            NetworkInfo info;
239            switch (message.what) {
240               case CMD_CONNECTIVITY_CHANGE:
241                    info = (NetworkInfo) message.obj;
242                    if (!info.isConnected()
243                            && info.getType() == mNetworkInfo.getType()) {
244                        if (DBG) log("Disconnected from active network " + info);
245                        transitionTo(mNoActiveNetworkState);
246                    } else if (info.getType() != mNetworkInfo.getType() &&
247                            info.isConnected() &&
248                            isActiveNetwork(info)) {
249                        if (DBG) log("Active network switched " + info);
250                        deferMessage(message);
251                        transitionTo(mNoActiveNetworkState);
252                    }
253                    break;
254                default:
255                    return NOT_HANDLED;
256            }
257            return HANDLED;
258        }
259    }
260
261
262
263    private class DelayedCaptiveCheckState extends State {
264        @Override
265        public void enter() {
266            Message message = obtainMessage(CMD_DELAYED_CAPTIVE_CHECK, ++mDelayedCheckToken, 0);
267            if (mDeviceProvisioned) {
268                sendMessageDelayed(message, DELAYED_CHECK_INTERVAL_MS);
269            } else {
270                sendMessage(message);
271            }
272        }
273
274        @Override
275        public boolean processMessage(Message message) {
276            if (DBG) log(getName() + message.toString());
277            switch (message.what) {
278                case CMD_DELAYED_CAPTIVE_CHECK:
279                    setNotificationOff();
280
281                    if (message.arg1 == mDelayedCheckToken) {
282                        InetAddress server = lookupHost(mServer);
283                        boolean captive = server != null && isCaptivePortal(server);
284                        if (captive) {
285                            if (DBG) log("Captive network " + mNetworkInfo);
286                        } else {
287                            if (DBG) log("Not captive network " + mNetworkInfo);
288                        }
289                        notifyPortalCheckCompleted(mNetworkInfo, captive);
290                        if (mDeviceProvisioned) {
291                            if (captive) {
292                                // Setup Wizard will assist the user in connecting to a captive
293                                // portal, so make the notification visible unless during setup
294                                try {
295                                    mConnService.setProvisioningNotificationVisible(true,
296                                        mNetworkInfo.getType(), mNetworkInfo.getExtraInfo(), mUrl);
297                                } catch(RemoteException e) {
298                                    e.printStackTrace();
299                                }
300                            }
301                        } else {
302                            Intent intent = new Intent(
303                                    ConnectivityManager.ACTION_CAPTIVE_PORTAL_TEST_COMPLETED);
304                            intent.putExtra(ConnectivityManager.EXTRA_IS_CAPTIVE_PORTAL, captive);
305                            intent.setPackage(SETUP_WIZARD_PACKAGE);
306                            mContext.sendBroadcast(intent);
307                        }
308
309                        transitionTo(mActiveNetworkState);
310                    }
311                    break;
312                default:
313                    return NOT_HANDLED;
314            }
315            return HANDLED;
316        }
317    }
318
319    private void notifyPortalCheckComplete(NetworkInfo info) {
320        if (info == null) {
321            loge("notifyPortalCheckComplete on null");
322            return;
323        }
324        try {
325            if (DBG) log("notifyPortalCheckComplete: ni=" + info);
326            mConnService.captivePortalCheckComplete(info);
327        } catch(RemoteException e) {
328            e.printStackTrace();
329        }
330    }
331
332    private void notifyPortalCheckCompleted(NetworkInfo info, boolean isCaptivePortal) {
333        if (info == null) {
334            loge("notifyPortalCheckComplete on null");
335            return;
336        }
337        try {
338            if (DBG) log("notifyPortalCheckCompleted: captive=" + isCaptivePortal + " ni=" + info);
339            mConnService.captivePortalCheckCompleted(info, isCaptivePortal);
340        } catch(RemoteException e) {
341            e.printStackTrace();
342        }
343    }
344
345    private boolean isActiveNetwork(NetworkInfo info) {
346        try {
347            NetworkInfo active = mConnService.getActiveNetworkInfo();
348            if (active != null && active.getType() == info.getType()) {
349                return true;
350            }
351        } catch (RemoteException e) {
352            e.printStackTrace();
353        }
354        return false;
355    }
356
357    private void setNotificationOff() {
358        try {
359            if (mNetworkInfo != null) {
360                mConnService.setProvisioningNotificationVisible(false, mNetworkInfo.getType(),
361                    null, null);
362            }
363        } catch (RemoteException e) {
364            log("setNotificationOff: " + e);
365        }
366    }
367
368    /**
369     * Do a URL fetch on a known server to see if we get the data we expect.
370     * Measure the response time and broadcast that.
371     */
372    private boolean isCaptivePortal(InetAddress server) {
373        HttpURLConnection urlConnection = null;
374        if (!mIsCaptivePortalCheckEnabled) return false;
375
376        mUrl = "http://" + server.getHostAddress() + "/generate_204";
377        if (DBG) log("Checking " + mUrl);
378        long requestTimestamp = -1;
379        try {
380            URL url = new URL(mUrl);
381            urlConnection = (HttpURLConnection) url.openConnection();
382            urlConnection.setInstanceFollowRedirects(false);
383            urlConnection.setConnectTimeout(SOCKET_TIMEOUT_MS);
384            urlConnection.setReadTimeout(SOCKET_TIMEOUT_MS);
385            urlConnection.setUseCaches(false);
386
387            // Time how long it takes to get a response to our request
388            requestTimestamp = SystemClock.elapsedRealtime();
389
390            urlConnection.getInputStream();
391
392            // Time how long it takes to get a response to our request
393            long responseTimestamp = SystemClock.elapsedRealtime();
394
395            // we got a valid response, but not from the real google
396            int rspCode = urlConnection.getResponseCode();
397            boolean isCaptivePortal = rspCode != 204;
398
399            sendNetworkConditionsBroadcast(true /* response received */, isCaptivePortal,
400                    requestTimestamp, responseTimestamp);
401
402            if (DBG) log("isCaptivePortal: ret=" + isCaptivePortal + " rspCode=" + rspCode);
403            return isCaptivePortal;
404        } catch (IOException e) {
405            if (DBG) log("Probably not a portal: exception " + e);
406            if (requestTimestamp != -1) {
407                sendFailedCaptivePortalCheckBroadcast(requestTimestamp);
408            } // else something went wrong with setting up the urlConnection
409            return false;
410        } finally {
411            if (urlConnection != null) {
412                urlConnection.disconnect();
413            }
414        }
415    }
416
417    private InetAddress lookupHost(String hostname) {
418        InetAddress inetAddress[];
419        try {
420            inetAddress = InetAddress.getAllByName(hostname);
421        } catch (UnknownHostException e) {
422            sendFailedCaptivePortalCheckBroadcast(SystemClock.elapsedRealtime());
423            return null;
424        }
425
426        for (InetAddress a : inetAddress) {
427            if (a instanceof Inet4Address) return a;
428        }
429
430        sendFailedCaptivePortalCheckBroadcast(SystemClock.elapsedRealtime());
431        return null;
432    }
433
434    private void sendFailedCaptivePortalCheckBroadcast(long requestTimestampMs) {
435        sendNetworkConditionsBroadcast(false /* response received */, false /* ignored */,
436                requestTimestampMs, 0 /* ignored */);
437    }
438
439    /**
440     * @param responseReceived - whether or not we received a valid HTTP response to our request.
441     * If false, isCaptivePortal and responseTimestampMs are ignored
442     */
443    private void sendNetworkConditionsBroadcast(boolean responseReceived, boolean isCaptivePortal,
444            long requestTimestampMs, long responseTimestampMs) {
445        if (Settings.Global.getInt(mContext.getContentResolver(),
446                Settings.Global.WIFI_SCAN_ALWAYS_AVAILABLE, 0) == 0) {
447            if (DBG) log("Don't send network conditions - lacking user consent.");
448            return;
449        }
450
451        Intent latencyBroadcast = new Intent(ACTION_NETWORK_CONDITIONS_MEASURED);
452        switch (mNetworkInfo.getType()) {
453            case ConnectivityManager.TYPE_WIFI:
454                WifiInfo currentWifiInfo = mWifiManager.getConnectionInfo();
455                if (currentWifiInfo != null) {
456                    latencyBroadcast.putExtra(EXTRA_SSID, currentWifiInfo.getSSID());
457                    latencyBroadcast.putExtra(EXTRA_BSSID, currentWifiInfo.getBSSID());
458                } else {
459                    if (DBG) logw("network info is TYPE_WIFI but no ConnectionInfo found");
460                    return;
461                }
462                break;
463            case ConnectivityManager.TYPE_MOBILE:
464                latencyBroadcast.putExtra(EXTRA_NETWORK_TYPE, mTelephonyManager.getNetworkType());
465                List<CellInfo> info = mTelephonyManager.getAllCellInfo();
466                if (info == null) return;
467                StringBuffer uniqueCellId = new StringBuffer();
468                int numRegisteredCellInfo = 0;
469                for (CellInfo cellInfo : info) {
470                    if (cellInfo.isRegistered()) {
471                        numRegisteredCellInfo++;
472                        if (numRegisteredCellInfo > 1) {
473                            if (DBG) log("more than one registered CellInfo.  Can't " +
474                                    "tell which is active.  Bailing.");
475                            return;
476                        }
477                        if (cellInfo instanceof CellInfoCdma) {
478                            CellIdentityCdma cellId = ((CellInfoCdma) cellInfo).getCellIdentity();
479                            latencyBroadcast.putExtra(EXTRA_CELL_ID, cellId);
480                        } else if (cellInfo instanceof CellInfoGsm) {
481                            CellIdentityGsm cellId = ((CellInfoGsm) cellInfo).getCellIdentity();
482                            latencyBroadcast.putExtra(EXTRA_CELL_ID, cellId);
483                        } else if (cellInfo instanceof CellInfoLte) {
484                            CellIdentityLte cellId = ((CellInfoLte) cellInfo).getCellIdentity();
485                            latencyBroadcast.putExtra(EXTRA_CELL_ID, cellId);
486                        } else if (cellInfo instanceof CellInfoWcdma) {
487                            CellIdentityWcdma cellId = ((CellInfoWcdma) cellInfo).getCellIdentity();
488                            latencyBroadcast.putExtra(EXTRA_CELL_ID, cellId);
489                        } else {
490                            if (DBG) logw("Registered cellinfo is unrecognized");
491                            return;
492                        }
493                    }
494                }
495                break;
496            default:
497                return;
498        }
499        latencyBroadcast.putExtra(EXTRA_CONNECTIVITY_TYPE, mNetworkInfo.getType());
500        latencyBroadcast.putExtra(EXTRA_RESPONSE_RECEIVED, responseReceived);
501        latencyBroadcast.putExtra(EXTRA_REQUEST_TIMESTAMP_MS, requestTimestampMs);
502
503        if (responseReceived) {
504            latencyBroadcast.putExtra(EXTRA_IS_CAPTIVE_PORTAL, isCaptivePortal);
505            latencyBroadcast.putExtra(EXTRA_RESPONSE_TIMESTAMP_MS, responseTimestampMs);
506        }
507        mContext.sendBroadcast(latencyBroadcast, PERMISSION_ACCESS_NETWORK_CONDITIONS);
508    }
509}
510