CaptivePortalTracker.java revision b8aad91f059527e04abaf8a83ed1ce6b5f09c55d
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.net.ConnectivityManager;
29import android.net.IConnectivityManager;
30import android.os.UserHandle;
31import android.os.Message;
32import android.os.RemoteException;
33import android.provider.Settings;
34import android.telephony.TelephonyManager;
35import android.util.Log;
36
37import com.android.internal.util.State;
38import com.android.internal.util.StateMachine;
39
40import java.io.IOException;
41import java.net.HttpURLConnection;
42import java.net.InetAddress;
43import java.net.Inet4Address;
44import java.net.URL;
45import java.net.UnknownHostException;
46
47import com.android.internal.R;
48
49/**
50 * This class allows captive portal detection on a network.
51 * @hide
52 */
53public class CaptivePortalTracker extends StateMachine {
54    private static final boolean DBG = false;
55    private static final String TAG = "CaptivePortalTracker";
56
57    private static final String DEFAULT_SERVER = "clients3.google.com";
58    private static final String NOTIFICATION_ID = "CaptivePortal.Notification";
59
60    private static final int SOCKET_TIMEOUT_MS = 10000;
61
62    private String mServer;
63    private String mUrl;
64    private boolean mNotificationShown = false;
65    private boolean mIsCaptivePortalCheckEnabled = false;
66    private IConnectivityManager mConnService;
67    private TelephonyManager mTelephonyManager;
68    private Context mContext;
69    private NetworkInfo mNetworkInfo;
70
71    private static final int CMD_DETECT_PORTAL          = 0;
72    private static final int CMD_CONNECTIVITY_CHANGE    = 1;
73    private static final int CMD_DELAYED_CAPTIVE_CHECK  = 2;
74
75    /* This delay happens every time before we do a captive check on a network */
76    private static final int DELAYED_CHECK_INTERVAL_MS = 10000;
77    private int mDelayedCheckToken = 0;
78
79    private State mDefaultState = new DefaultState();
80    private State mNoActiveNetworkState = new NoActiveNetworkState();
81    private State mActiveNetworkState = new ActiveNetworkState();
82    private State mDelayedCaptiveCheckState = new DelayedCaptiveCheckState();
83
84    private CaptivePortalTracker(Context context, IConnectivityManager cs) {
85        super(TAG);
86
87        mContext = context;
88        mConnService = cs;
89        mTelephonyManager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
90
91        IntentFilter filter = new IntentFilter();
92        filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION);
93        mContext.registerReceiver(mReceiver, filter);
94
95        mServer = Settings.Global.getString(mContext.getContentResolver(),
96                Settings.Global.CAPTIVE_PORTAL_SERVER);
97        if (mServer == null) mServer = DEFAULT_SERVER;
98
99        mIsCaptivePortalCheckEnabled = Settings.Global.getInt(mContext.getContentResolver(),
100                Settings.Global.CAPTIVE_PORTAL_DETECTION_ENABLED, 1) == 1;
101
102        addState(mDefaultState);
103            addState(mNoActiveNetworkState, mDefaultState);
104            addState(mActiveNetworkState, mDefaultState);
105                addState(mDelayedCaptiveCheckState, mActiveNetworkState);
106        setInitialState(mNoActiveNetworkState);
107    }
108
109    private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
110        @Override
111        public void onReceive(Context context, Intent intent) {
112            String action = intent.getAction();
113            if (action.equals(ConnectivityManager.CONNECTIVITY_ACTION)) {
114                NetworkInfo info = intent.getParcelableExtra(
115                        ConnectivityManager.EXTRA_NETWORK_INFO);
116                sendMessage(obtainMessage(CMD_CONNECTIVITY_CHANGE, info));
117            }
118        }
119    };
120
121    public static CaptivePortalTracker makeCaptivePortalTracker(Context context,
122            IConnectivityManager cs) {
123        CaptivePortalTracker captivePortal = new CaptivePortalTracker(context, cs);
124        captivePortal.start();
125        return captivePortal;
126    }
127
128    public void detectCaptivePortal(NetworkInfo info) {
129        sendMessage(obtainMessage(CMD_DETECT_PORTAL, info));
130    }
131
132    private class DefaultState extends State {
133        @Override
134        public void enter() {
135            if (DBG) log(getName() + "\n");
136        }
137
138        @Override
139        public boolean processMessage(Message message) {
140            if (DBG) log(getName() + message.toString() + "\n");
141            switch (message.what) {
142                case CMD_DETECT_PORTAL:
143                    NetworkInfo info = (NetworkInfo) message.obj;
144                    // Checking on a secondary connection is not supported
145                    // yet
146                    notifyPortalCheckComplete(info);
147                    break;
148                case CMD_CONNECTIVITY_CHANGE:
149                case CMD_DELAYED_CAPTIVE_CHECK:
150                    break;
151                default:
152                    loge("Ignoring " + message);
153                    break;
154            }
155            return HANDLED;
156        }
157    }
158
159    private class NoActiveNetworkState extends State {
160        @Override
161        public void enter() {
162            if (DBG) log(getName() + "\n");
163            mNetworkInfo = null;
164            /* Clear any previous notification */
165            setNotificationVisible(false);
166        }
167
168        @Override
169        public boolean processMessage(Message message) {
170            if (DBG) log(getName() + message.toString() + "\n");
171            InetAddress server;
172            NetworkInfo info;
173            switch (message.what) {
174                case CMD_CONNECTIVITY_CHANGE:
175                    info = (NetworkInfo) message.obj;
176                    if (info.isConnected() && isActiveNetwork(info)) {
177                        mNetworkInfo = info;
178                        transitionTo(mDelayedCaptiveCheckState);
179                    }
180                    break;
181                default:
182                    return NOT_HANDLED;
183            }
184            return HANDLED;
185        }
186    }
187
188    private class ActiveNetworkState extends State {
189        @Override
190        public void enter() {
191            if (DBG) log(getName() + "\n");
192        }
193
194        @Override
195        public boolean processMessage(Message message) {
196            NetworkInfo info;
197            switch (message.what) {
198               case CMD_CONNECTIVITY_CHANGE:
199                    info = (NetworkInfo) message.obj;
200                    if (!info.isConnected()
201                            && info.getType() == mNetworkInfo.getType()) {
202                        if (DBG) log("Disconnected from active network " + info);
203                        transitionTo(mNoActiveNetworkState);
204                    } else if (info.getType() != mNetworkInfo.getType() &&
205                            info.isConnected() &&
206                            isActiveNetwork(info)) {
207                        if (DBG) log("Active network switched " + info);
208                        deferMessage(message);
209                        transitionTo(mNoActiveNetworkState);
210                    }
211                    break;
212                default:
213                    return NOT_HANDLED;
214            }
215            return HANDLED;
216        }
217    }
218
219
220
221    private class DelayedCaptiveCheckState extends State {
222        @Override
223        public void enter() {
224            if (DBG) log(getName() + "\n");
225            sendMessageDelayed(obtainMessage(CMD_DELAYED_CAPTIVE_CHECK,
226                        ++mDelayedCheckToken, 0), DELAYED_CHECK_INTERVAL_MS);
227        }
228
229        @Override
230        public boolean processMessage(Message message) {
231            if (DBG) log(getName() + message.toString() + "\n");
232            switch (message.what) {
233                case CMD_DELAYED_CAPTIVE_CHECK:
234                    if (message.arg1 == mDelayedCheckToken) {
235                        InetAddress server = lookupHost(mServer);
236                        if (server != null) {
237                            if (isCaptivePortal(server)) {
238                                if (DBG) log("Captive network " + mNetworkInfo);
239                                setNotificationVisible(true);
240                            }
241                        }
242                        if (DBG) log("Not captive network " + mNetworkInfo);
243                        transitionTo(mActiveNetworkState);
244                    }
245                    break;
246                default:
247                    return NOT_HANDLED;
248            }
249            return HANDLED;
250        }
251    }
252
253    private void notifyPortalCheckComplete(NetworkInfo info) {
254        if (info == null) {
255            loge("notifyPortalCheckComplete on null");
256            return;
257        }
258        try {
259            mConnService.captivePortalCheckComplete(info);
260        } catch(RemoteException e) {
261            e.printStackTrace();
262        }
263    }
264
265    private boolean isActiveNetwork(NetworkInfo info) {
266        try {
267            NetworkInfo active = mConnService.getActiveNetworkInfo();
268            if (active != null && active.getType() == info.getType()) {
269                return true;
270            }
271        } catch (RemoteException e) {
272            e.printStackTrace();
273        }
274        return false;
275    }
276
277    /**
278     * Do a URL fetch on a known server to see if we get the data we expect
279     */
280    private boolean isCaptivePortal(InetAddress server) {
281        HttpURLConnection urlConnection = null;
282        if (!mIsCaptivePortalCheckEnabled) return false;
283
284        mUrl = "http://" + server.getHostAddress() + "/generate_204";
285        if (DBG) log("Checking " + mUrl);
286        try {
287            URL url = new URL(mUrl);
288            urlConnection = (HttpURLConnection) url.openConnection();
289            urlConnection.setInstanceFollowRedirects(false);
290            urlConnection.setConnectTimeout(SOCKET_TIMEOUT_MS);
291            urlConnection.setReadTimeout(SOCKET_TIMEOUT_MS);
292            urlConnection.setUseCaches(false);
293            urlConnection.getInputStream();
294            // we got a valid response, but not from the real google
295            return urlConnection.getResponseCode() != 204;
296        } catch (IOException e) {
297            if (DBG) log("Probably not a portal: exception " + e);
298            return false;
299        } finally {
300            if (urlConnection != null) {
301                urlConnection.disconnect();
302            }
303        }
304    }
305
306    private InetAddress lookupHost(String hostname) {
307        InetAddress inetAddress[];
308        try {
309            inetAddress = InetAddress.getAllByName(hostname);
310        } catch (UnknownHostException e) {
311            return null;
312        }
313
314        for (InetAddress a : inetAddress) {
315            if (a instanceof Inet4Address) return a;
316        }
317        return null;
318    }
319
320    private void setNotificationVisible(boolean visible) {
321        // if it should be hidden and it is already hidden, then noop
322        if (!visible && !mNotificationShown) {
323            return;
324        }
325
326        Resources r = Resources.getSystem();
327        NotificationManager notificationManager = (NotificationManager) mContext
328            .getSystemService(Context.NOTIFICATION_SERVICE);
329
330        if (visible) {
331            CharSequence title;
332            CharSequence details;
333            switch (mNetworkInfo.getType()) {
334                case ConnectivityManager.TYPE_WIFI:
335                    title = r.getString(R.string.wifi_available_sign_in, 0);
336                    details = r.getString(R.string.network_available_sign_in_detailed,
337                            mNetworkInfo.getExtraInfo());
338                    break;
339                case ConnectivityManager.TYPE_MOBILE:
340                    title = r.getString(R.string.network_available_sign_in, 0);
341                    // TODO: Change this to pull from NetworkInfo once a printable
342                    // name has been added to it
343                    details = mTelephonyManager.getNetworkOperatorName();
344                    break;
345                default:
346                    title = r.getString(R.string.network_available_sign_in, 0);
347                    details = r.getString(R.string.network_available_sign_in_detailed,
348                            mNetworkInfo.getExtraInfo());
349                    break;
350            }
351
352            Notification notification = new Notification();
353            notification.when = 0;
354            notification.icon = com.android.internal.R.drawable.stat_notify_wifi_in_range;
355            notification.flags = Notification.FLAG_AUTO_CANCEL;
356            Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(mUrl));
357            intent.setFlags(Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT |
358                    Intent.FLAG_ACTIVITY_NEW_TASK);
359            notification.contentIntent = PendingIntent.getActivity(mContext, 0, intent, 0);
360            notification.tickerText = title;
361            notification.setLatestEventInfo(mContext, title, details, notification.contentIntent);
362
363            notificationManager.notify(NOTIFICATION_ID, 1, notification);
364        } else {
365            notificationManager.cancel(NOTIFICATION_ID, 1);
366        }
367        mNotificationShown = visible;
368    }
369
370    private static void log(String s) {
371        Log.d(TAG, s);
372    }
373
374    private static void loge(String s) {
375        Log.e(TAG, s);
376    }
377
378}
379