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