CaptivePortalTracker.java revision 7f0aaac7f2a2bff6168467132c704fce2c7ca170
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.Notification;
20import android.app.NotificationManager;
21import android.app.PendingIntent;
22import android.content.BroadcastReceiver;
23import android.content.Context;
24import android.content.Intent;
25import android.content.IntentFilter;
26import android.content.res.Resources;
27import android.database.ContentObserver;
28import android.net.ConnectivityManager;
29import android.net.IConnectivityManager;
30import android.os.Handler;
31import android.os.HandlerThread;
32import android.os.Looper;
33import android.os.Message;
34import android.os.RemoteException;
35import android.provider.Settings;
36import android.util.Log;
37
38import java.io.IOException;
39import java.net.HttpURLConnection;
40import java.net.InetAddress;
41import java.net.Inet4Address;
42import java.net.URL;
43import java.net.UnknownHostException;
44import java.util.concurrent.atomic.AtomicBoolean;
45
46import com.android.internal.R;
47
48/**
49 * This class allows captive portal detection
50 * @hide
51 */
52public class CaptivePortalTracker {
53    private static final boolean DBG = true;
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 InternalHandler mHandler;
66    private IConnectivityManager mConnService;
67    private Context mContext;
68    private NetworkInfo mNetworkInfo;
69    private boolean mIsCaptivePortal = false;
70
71    private static final int DETECT_PORTAL = 0;
72    private static final int HANDLE_CONNECT = 1;
73
74    /**
75     * Activity Action: Switch to the captive portal network
76     * <p>Input: Nothing.
77     * <p>Output: Nothing.
78     */
79    public static final String ACTION_SWITCH_TO_CAPTIVE_PORTAL
80            = "android.net.SWITCH_TO_CAPTIVE_PORTAL";
81
82    private CaptivePortalTracker(Context context, NetworkInfo info, IConnectivityManager cs) {
83        mContext = context;
84        mNetworkInfo = info;
85        mConnService = cs;
86
87        HandlerThread handlerThread = new HandlerThread("CaptivePortalThread");
88        handlerThread.start();
89        mHandler = new InternalHandler(handlerThread.getLooper());
90        mHandler.obtainMessage(DETECT_PORTAL).sendToTarget();
91
92        IntentFilter filter = new IntentFilter();
93        filter.addAction(ACTION_SWITCH_TO_CAPTIVE_PORTAL);
94        filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION);
95
96        mContext.registerReceiver(mReceiver, filter);
97
98        mServer = Settings.Secure.getString(mContext.getContentResolver(),
99                Settings.Secure.CAPTIVE_PORTAL_SERVER);
100        if (mServer == null) mServer = DEFAULT_SERVER;
101
102        mIsCaptivePortalCheckEnabled = Settings.Secure.getInt(mContext.getContentResolver(),
103                Settings.Secure.CAPTIVE_PORTAL_DETECTION_ENABLED, 1) == 1;
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(ACTION_SWITCH_TO_CAPTIVE_PORTAL)) {
111                notifyPortalCheckComplete();
112            } else if (action.equals(ConnectivityManager.CONNECTIVITY_ACTION)) {
113                NetworkInfo info = intent.getParcelableExtra(
114                        ConnectivityManager.EXTRA_NETWORK_INFO);
115                mHandler.obtainMessage(HANDLE_CONNECT, info).sendToTarget();
116            }
117        }
118    };
119
120    public static CaptivePortalTracker detect(Context context, NetworkInfo info,
121            IConnectivityManager cs) {
122        CaptivePortalTracker captivePortal = new CaptivePortalTracker(context, info, cs);
123        return captivePortal;
124    }
125
126    private class InternalHandler extends Handler {
127        public InternalHandler(Looper looper) {
128            super(looper);
129        }
130
131        @Override
132        public void handleMessage(Message msg) {
133            switch (msg.what) {
134                case DETECT_PORTAL:
135                    InetAddress server = lookupHost(mServer);
136                    if (server != null) {
137                        requestRouteToHost(server);
138                        if (isCaptivePortal(server)) {
139                            if (DBG) log("Captive portal " + mNetworkInfo);
140                            setNotificationVisible(true);
141                            mIsCaptivePortal = true;
142                            break;
143                        }
144                    }
145                    notifyPortalCheckComplete();
146                    quit();
147                    break;
148                case HANDLE_CONNECT:
149                    NetworkInfo info = (NetworkInfo) msg.obj;
150                    if (info.getType() != mNetworkInfo.getType()) break;
151
152                    if (info.getState() == NetworkInfo.State.CONNECTED ||
153                            info.getState() == NetworkInfo.State.DISCONNECTED) {
154                        setNotificationVisible(false);
155                    }
156
157                    /* Connected to a captive portal */
158                    if (info.getState() == NetworkInfo.State.CONNECTED &&
159                            mIsCaptivePortal) {
160                        launchBrowser();
161                        quit();
162                    }
163                    break;
164                default:
165                    loge("Unhandled message " + msg);
166                    break;
167            }
168        }
169
170        private void quit() {
171            mIsCaptivePortal = false;
172            getLooper().quit();
173            mContext.unregisterReceiver(mReceiver);
174        }
175    }
176
177    private void launchBrowser() {
178        Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(mUrl));
179        intent.setFlags(Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT | Intent.FLAG_ACTIVITY_NEW_TASK);
180        mContext.startActivity(intent);
181    }
182
183    private void notifyPortalCheckComplete() {
184        try {
185            mConnService.captivePortalCheckComplete(mNetworkInfo);
186        } catch(RemoteException e) {
187            e.printStackTrace();
188        }
189    }
190
191    private void requestRouteToHost(InetAddress server) {
192        try {
193            mConnService.requestRouteToHostAddress(mNetworkInfo.getType(),
194                    server.getAddress());
195        } catch (RemoteException e) {
196            e.printStackTrace();
197        }
198    }
199
200    /**
201     * Do a URL fetch on a known server to see if we get the data we expect
202     */
203    private boolean isCaptivePortal(InetAddress server) {
204        HttpURLConnection urlConnection = null;
205        if (!mIsCaptivePortalCheckEnabled) return false;
206
207        mUrl = "http://" + server.getHostAddress() + "/generate_204";
208        try {
209            URL url = new URL(mUrl);
210            urlConnection = (HttpURLConnection) url.openConnection();
211            urlConnection.setInstanceFollowRedirects(false);
212            urlConnection.setConnectTimeout(SOCKET_TIMEOUT_MS);
213            urlConnection.setReadTimeout(SOCKET_TIMEOUT_MS);
214            urlConnection.setUseCaches(false);
215            urlConnection.getInputStream();
216            // we got a valid response, but not from the real google
217            return urlConnection.getResponseCode() != 204;
218        } catch (IOException e) {
219            if (DBG) log("Probably not a portal: exception " + e);
220            return false;
221        } finally {
222            if (urlConnection != null) {
223                urlConnection.disconnect();
224            }
225        }
226    }
227
228    private InetAddress lookupHost(String hostname) {
229        InetAddress inetAddress[];
230        try {
231            inetAddress = InetAddress.getAllByName(hostname);
232        } catch (UnknownHostException e) {
233            return null;
234        }
235
236        for (InetAddress a : inetAddress) {
237            if (a instanceof Inet4Address) return a;
238        }
239        return null;
240    }
241
242    private void setNotificationVisible(boolean visible) {
243        // if it should be hidden and it is already hidden, then noop
244        if (!visible && !mNotificationShown) {
245            return;
246        }
247
248        Resources r = Resources.getSystem();
249        NotificationManager notificationManager = (NotificationManager) mContext
250            .getSystemService(Context.NOTIFICATION_SERVICE);
251
252        if (visible) {
253            CharSequence title = r.getString(R.string.wifi_available_sign_in, 0);
254            CharSequence details = r.getString(R.string.network_available_sign_in_detailed,
255                    mNetworkInfo.getExtraInfo());
256
257            Notification notification = new Notification();
258            notification.when = 0;
259            notification.icon = com.android.internal.R.drawable.stat_notify_wifi_in_range;
260            notification.flags = Notification.FLAG_AUTO_CANCEL;
261            notification.contentIntent = PendingIntent.getBroadcast(mContext, 0,
262                    new Intent(CaptivePortalTracker.ACTION_SWITCH_TO_CAPTIVE_PORTAL), 0);
263
264            notification.tickerText = title;
265            notification.setLatestEventInfo(mContext, title, details, notification.contentIntent);
266
267            notificationManager.notify(NOTIFICATION_ID, 1, notification);
268        } else {
269            notificationManager.cancel(NOTIFICATION_ID, 1);
270        }
271        mNotificationShown = visible;
272    }
273
274    private static void log(String s) {
275        Log.d(TAG, s);
276    }
277
278    private static void loge(String s) {
279        Log.e(TAG, s);
280    }
281
282}
283