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