Skip to content

Commit da6da09

Browse files
committed
Captive portal handling
We now notify the user of a captive portal before switching to the network as default. This allows background applications to continue to work until the user confirms he wants to sign in to the captive portal. Also, moved out captive portal handling out of wifi as a seperate component. Change-Id: I7c7507481967e33a1afad0b4961688bd192f0d31
1 parent 10a0df8 commit da6da09

18 files changed

+472
-232
lines changed

api/current.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12606,6 +12606,7 @@ package android.net {
1260612606
method public static final android.net.NetworkInfo.DetailedState[] values();
1260712607
enum_constant public static final android.net.NetworkInfo.DetailedState AUTHENTICATING;
1260812608
enum_constant public static final android.net.NetworkInfo.DetailedState BLOCKED;
12609+
enum_constant public static final android.net.NetworkInfo.DetailedState CAPTIVE_PORTAL_CHECK;
1260912610
enum_constant public static final android.net.NetworkInfo.DetailedState CONNECTED;
1261012611
enum_constant public static final android.net.NetworkInfo.DetailedState CONNECTING;
1261112612
enum_constant public static final android.net.NetworkInfo.DetailedState DISCONNECTED;

core/java/android/bluetooth/BluetoothTetheringDataTracker.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,11 @@ public boolean teardown() {
133133
return true;
134134
}
135135

136+
@Override
137+
public void captivePortalCheckComplete() {
138+
// not implemented
139+
}
140+
136141
/**
137142
* Re-enable connectivity to a network after a {@link #teardown()}.
138143
*/
Lines changed: 282 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,282 @@
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+
17+
package android.net;
18+
19+
import android.app.Notification;
20+
import android.app.NotificationManager;
21+
import android.app.PendingIntent;
22+
import android.content.BroadcastReceiver;
23+
import android.content.Context;
24+
import android.content.Intent;
25+
import android.content.IntentFilter;
26+
import android.content.res.Resources;
27+
import android.database.ContentObserver;
28+
import android.net.ConnectivityManager;
29+
import android.net.IConnectivityManager;
30+
import android.os.Handler;
31+
import android.os.HandlerThread;
32+
import android.os.Looper;
33+
import android.os.Message;
34+
import android.os.RemoteException;
35+
import android.provider.Settings;
36+
import android.util.Log;
37+
38+
import java.io.IOException;
39+
import java.net.HttpURLConnection;
40+
import java.net.InetAddress;
41+
import java.net.Inet4Address;
42+
import java.net.URL;
43+
import java.net.UnknownHostException;
44+
import java.util.concurrent.atomic.AtomicBoolean;
45+
46+
import com.android.internal.R;
47+
48+
/**
49+
* This class allows captive portal detection
50+
* @hide
51+
*/
52+
public 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.wifi_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+
}

core/java/android/net/ConnectivityManager.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -921,4 +921,15 @@ public boolean updateLockdownVpn() {
921921
return false;
922922
}
923923
}
924+
925+
/**
926+
* {@hide}
927+
*/
928+
public void captivePortalCheckComplete(NetworkInfo info) {
929+
try {
930+
mService.captivePortalCheckComplete(info);
931+
} catch (RemoteException e) {
932+
}
933+
}
934+
924935
}

core/java/android/net/DummyDataStateTracker.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,10 @@ public boolean teardown() {
119119
return true;
120120
}
121121

122+
public void captivePortalCheckComplete() {
123+
// not implemented
124+
}
125+
122126
/**
123127
* Record the detailed state of a network, and if it is a
124128
* change from the previous state, send a notification to

core/java/android/net/EthernetDataTracker.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,11 @@ public boolean reconnect() {
274274
return mLinkUp;
275275
}
276276

277+
@Override
278+
public void captivePortalCheckComplete() {
279+
// not implemented
280+
}
281+
277282
/**
278283
* Turn the wireless radio off for a network.
279284
* @param turnOn {@code true} to turn the radio on, {@code false}

core/java/android/net/IConnectivityManager.aidl

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,4 +124,6 @@ interface IConnectivityManager
124124
LegacyVpnInfo getLegacyVpnInfo();
125125

126126
boolean updateLockdownVpn();
127+
128+
void captivePortalCheckComplete(in NetworkInfo info);
127129
}

core/java/android/net/MobileDataStateTracker.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -381,6 +381,11 @@ public boolean teardown() {
381381
return (setEnableApn(mApnType, false) != PhoneConstants.APN_REQUEST_FAILED);
382382
}
383383

384+
@Override
385+
public void captivePortalCheckComplete() {
386+
// not implemented
387+
}
388+
384389
/**
385390
* Record the detailed state of a network, and if it is a
386391
* change from the previous state, send a notification to

core/java/android/net/NetworkInfo.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,9 @@ public enum DetailedState {
7979
/** Access to this network is blocked. */
8080
BLOCKED,
8181
/** Link has poor connectivity. */
82-
VERIFYING_POOR_LINK
82+
VERIFYING_POOR_LINK,
83+
/** Checking if network is a captive portal */
84+
CAPTIVE_PORTAL_CHECK,
8385
}
8486

8587
/**
@@ -97,6 +99,7 @@ public enum DetailedState {
9799
stateMap.put(DetailedState.AUTHENTICATING, State.CONNECTING);
98100
stateMap.put(DetailedState.OBTAINING_IPADDR, State.CONNECTING);
99101
stateMap.put(DetailedState.VERIFYING_POOR_LINK, State.CONNECTING);
102+
stateMap.put(DetailedState.CAPTIVE_PORTAL_CHECK, State.CONNECTING);
100103
stateMap.put(DetailedState.CONNECTED, State.CONNECTED);
101104
stateMap.put(DetailedState.SUSPENDED, State.SUSPENDED);
102105
stateMap.put(DetailedState.DISCONNECTING, State.DISCONNECTING);

core/java/android/net/NetworkStateTracker.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,11 @@ public interface NetworkStateTracker {
122122
*/
123123
public boolean reconnect();
124124

125+
/**
126+
* Ready to switch on to the network after captive portal check
127+
*/
128+
public void captivePortalCheckComplete();
129+
125130
/**
126131
* Turn the wireless radio off for a network.
127132
* @param turnOn {@code true} to turn the radio on, {@code false}

0 commit comments

Comments
 (0)