CaptivePortalTracker.java revision 108da0cfa4a2f59cc953a4ec61314e69b61d6777
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;
37
38import com.android.internal.util.State;
39import com.android.internal.util.StateMachine;
40
41import java.io.IOException;
42import java.net.HttpURLConnection;
43import java.net.InetAddress;
44import java.net.Inet4Address;
45import java.net.URL;
46import java.net.UnknownHostException;
47
48import com.android.internal.R;
49
50/**
51 * This class allows captive portal detection on a network.
52 * @hide
53 */
54public class CaptivePortalTracker extends StateMachine {
55    private static final boolean DBG = false;
56    private static final String TAG = "CaptivePortalTracker";
57
58    private static final String DEFAULT_SERVER = "clients3.google.com";
59    private static final String NOTIFICATION_ID = "CaptivePortal.Notification";
60
61    private static final int SOCKET_TIMEOUT_MS = 10000;
62
63    private String mServer;
64    private String mUrl;
65    private boolean mNotificationShown = false;
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            if (DBG) log(getName() + "\n");
163        }
164
165        @Override
166        public boolean processMessage(Message message) {
167            if (DBG) log(getName() + message.toString() + "\n");
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            if (DBG) log(getName() + "\n");
190            mNetworkInfo = null;
191            /* Clear any previous notification */
192            setNotificationVisible(false);
193        }
194
195        @Override
196        public boolean processMessage(Message message) {
197            if (DBG) log(getName() + message.toString() + "\n");
198            InetAddress server;
199            NetworkInfo info;
200            switch (message.what) {
201                case CMD_CONNECTIVITY_CHANGE:
202                    info = (NetworkInfo) message.obj;
203                    if (info.isConnected() && isActiveNetwork(info)) {
204                        mNetworkInfo = info;
205                        transitionTo(mDelayedCaptiveCheckState);
206                    }
207                    break;
208                default:
209                    return NOT_HANDLED;
210            }
211            return HANDLED;
212        }
213    }
214
215    private class ActiveNetworkState extends State {
216        @Override
217        public void enter() {
218            if (DBG) log(getName() + "\n");
219        }
220
221        @Override
222        public boolean processMessage(Message message) {
223            NetworkInfo info;
224            switch (message.what) {
225               case CMD_CONNECTIVITY_CHANGE:
226                    info = (NetworkInfo) message.obj;
227                    if (!info.isConnected()
228                            && info.getType() == mNetworkInfo.getType()) {
229                        if (DBG) log("Disconnected from active network " + info);
230                        transitionTo(mNoActiveNetworkState);
231                    } else if (info.getType() != mNetworkInfo.getType() &&
232                            info.isConnected() &&
233                            isActiveNetwork(info)) {
234                        if (DBG) log("Active network switched " + info);
235                        deferMessage(message);
236                        transitionTo(mNoActiveNetworkState);
237                    }
238                    break;
239                default:
240                    return NOT_HANDLED;
241            }
242            return HANDLED;
243        }
244    }
245
246
247
248    private class DelayedCaptiveCheckState extends State {
249        @Override
250        public void enter() {
251            if (DBG) log(getName() + "\n");
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() + "\n");
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                        if (mDeviceProvisioned) {
274                            if (captive) {
275                                // Setup Wizard will assist the user in connecting to a captive
276                                // portal, so make the notification visible unless during setup
277                                setNotificationVisible(true);
278                            }
279                        } else {
280                            Intent intent = new Intent(
281                                    ConnectivityManager.ACTION_CAPTIVE_PORTAL_TEST_COMPLETED);
282                            intent.putExtra(ConnectivityManager.EXTRA_IS_CAPTIVE_PORTAL, captive);
283                            intent.setPackage(SETUP_WIZARD_PACKAGE);
284                            mContext.sendBroadcast(intent);
285                        }
286
287                        transitionTo(mActiveNetworkState);
288                    }
289                    break;
290                default:
291                    return NOT_HANDLED;
292            }
293            return HANDLED;
294        }
295    }
296
297    private void notifyPortalCheckComplete(NetworkInfo info) {
298        if (info == null) {
299            loge("notifyPortalCheckComplete on null");
300            return;
301        }
302        try {
303            mConnService.captivePortalCheckComplete(info);
304        } catch(RemoteException e) {
305            e.printStackTrace();
306        }
307    }
308
309    private boolean isActiveNetwork(NetworkInfo info) {
310        try {
311            NetworkInfo active = mConnService.getActiveNetworkInfo();
312            if (active != null && active.getType() == info.getType()) {
313                return true;
314            }
315        } catch (RemoteException e) {
316            e.printStackTrace();
317        }
318        return false;
319    }
320
321    /**
322     * Do a URL fetch on a known server to see if we get the data we expect
323     */
324    private boolean isCaptivePortal(InetAddress server) {
325        HttpURLConnection urlConnection = null;
326        if (!mIsCaptivePortalCheckEnabled) return false;
327
328        mUrl = "http://" + server.getHostAddress() + "/generate_204";
329        if (DBG) log("Checking " + mUrl);
330        try {
331            URL url = new URL(mUrl);
332            urlConnection = (HttpURLConnection) url.openConnection();
333            urlConnection.setInstanceFollowRedirects(false);
334            urlConnection.setConnectTimeout(SOCKET_TIMEOUT_MS);
335            urlConnection.setReadTimeout(SOCKET_TIMEOUT_MS);
336            urlConnection.setUseCaches(false);
337            urlConnection.getInputStream();
338            // we got a valid response, but not from the real google
339            return urlConnection.getResponseCode() != 204;
340        } catch (IOException e) {
341            if (DBG) log("Probably not a portal: exception " + e);
342            return false;
343        } finally {
344            if (urlConnection != null) {
345                urlConnection.disconnect();
346            }
347        }
348    }
349
350    private InetAddress lookupHost(String hostname) {
351        InetAddress inetAddress[];
352        try {
353            inetAddress = InetAddress.getAllByName(hostname);
354        } catch (UnknownHostException e) {
355            return null;
356        }
357
358        for (InetAddress a : inetAddress) {
359            if (a instanceof Inet4Address) return a;
360        }
361        return null;
362    }
363
364    private void setNotificationVisible(boolean visible) {
365        // if it should be hidden and it is already hidden, then noop
366        if (!visible && !mNotificationShown) {
367            return;
368        }
369
370        Resources r = Resources.getSystem();
371        NotificationManager notificationManager = (NotificationManager) mContext
372            .getSystemService(Context.NOTIFICATION_SERVICE);
373
374        if (visible) {
375            CharSequence title;
376            CharSequence details;
377            int icon;
378            switch (mNetworkInfo.getType()) {
379                case ConnectivityManager.TYPE_WIFI:
380                    title = r.getString(R.string.wifi_available_sign_in, 0);
381                    details = r.getString(R.string.network_available_sign_in_detailed,
382                            mNetworkInfo.getExtraInfo());
383                    icon = R.drawable.stat_notify_wifi_in_range;
384                    break;
385                case ConnectivityManager.TYPE_MOBILE:
386                    title = r.getString(R.string.network_available_sign_in, 0);
387                    // TODO: Change this to pull from NetworkInfo once a printable
388                    // name has been added to it
389                    details = mTelephonyManager.getNetworkOperatorName();
390                    icon = R.drawable.stat_notify_rssi_in_range;
391                    break;
392                default:
393                    title = r.getString(R.string.network_available_sign_in, 0);
394                    details = r.getString(R.string.network_available_sign_in_detailed,
395                            mNetworkInfo.getExtraInfo());
396                    icon = R.drawable.stat_notify_rssi_in_range;
397                    break;
398            }
399
400            Notification notification = new Notification();
401            notification.when = 0;
402            notification.icon = icon;
403            notification.flags = Notification.FLAG_AUTO_CANCEL;
404            Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(mUrl));
405            intent.setFlags(Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT |
406                    Intent.FLAG_ACTIVITY_NEW_TASK);
407            notification.contentIntent = PendingIntent.getActivity(mContext, 0, intent, 0);
408            notification.tickerText = title;
409            notification.setLatestEventInfo(mContext, title, details, notification.contentIntent);
410
411            notificationManager.notify(NOTIFICATION_ID, 1, notification);
412        } else {
413            notificationManager.cancel(NOTIFICATION_ID, 1);
414        }
415        mNotificationShown = visible;
416    }
417}
418