diff options
author | Irfan Sheriff <isheriff@google.com> | 2012-08-16 12:49:23 -0700 |
---|---|---|
committer | Irfan Sheriff <isheriff@google.com> | 2012-08-27 22:27:06 -0700 |
commit | da6da0907b28d4704aabbdb1bbeb4300954670d1 (patch) | |
tree | 58a2b58cf777d02d0d89cc8f54b0ce5d29a9c5b2 | |
parent | 10a0df8459d22b29fb9163071e8cbc2bb7194393 (diff) | |
download | frameworks_base-da6da0907b28d4704aabbdb1bbeb4300954670d1.zip frameworks_base-da6da0907b28d4704aabbdb1bbeb4300954670d1.tar.gz frameworks_base-da6da0907b28d4704aabbdb1bbeb4300954670d1.tar.bz2 |
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
18 files changed, 472 insertions, 232 deletions
diff --git a/api/current.txt b/api/current.txt index 512a311..4a5f071 100644 --- a/api/current.txt +++ b/api/current.txt @@ -12606,6 +12606,7 @@ package android.net { method public static final android.net.NetworkInfo.DetailedState[] values(); enum_constant public static final android.net.NetworkInfo.DetailedState AUTHENTICATING; enum_constant public static final android.net.NetworkInfo.DetailedState BLOCKED; + enum_constant public static final android.net.NetworkInfo.DetailedState CAPTIVE_PORTAL_CHECK; enum_constant public static final android.net.NetworkInfo.DetailedState CONNECTED; enum_constant public static final android.net.NetworkInfo.DetailedState CONNECTING; enum_constant public static final android.net.NetworkInfo.DetailedState DISCONNECTED; diff --git a/core/java/android/bluetooth/BluetoothTetheringDataTracker.java b/core/java/android/bluetooth/BluetoothTetheringDataTracker.java index b2b5d81..30406e9 100644 --- a/core/java/android/bluetooth/BluetoothTetheringDataTracker.java +++ b/core/java/android/bluetooth/BluetoothTetheringDataTracker.java @@ -133,6 +133,11 @@ public class BluetoothTetheringDataTracker implements NetworkStateTracker { return true; } + @Override + public void captivePortalCheckComplete() { + // not implemented + } + /** * Re-enable connectivity to a network after a {@link #teardown()}. */ diff --git a/core/java/android/net/CaptivePortalTracker.java b/core/java/android/net/CaptivePortalTracker.java new file mode 100644 index 0000000..aa392d0 --- /dev/null +++ b/core/java/android/net/CaptivePortalTracker.java @@ -0,0 +1,282 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.net; + +import android.app.Notification; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.res.Resources; +import android.database.ContentObserver; +import android.net.ConnectivityManager; +import android.net.IConnectivityManager; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Looper; +import android.os.Message; +import android.os.RemoteException; +import android.provider.Settings; +import android.util.Log; + +import java.io.IOException; +import java.net.HttpURLConnection; +import java.net.InetAddress; +import java.net.Inet4Address; +import java.net.URL; +import java.net.UnknownHostException; +import java.util.concurrent.atomic.AtomicBoolean; + +import com.android.internal.R; + +/** + * This class allows captive portal detection + * @hide + */ +public class CaptivePortalTracker { + private static final boolean DBG = true; + private static final String TAG = "CaptivePortalTracker"; + + private static final String DEFAULT_SERVER = "clients3.google.com"; + private static final String NOTIFICATION_ID = "CaptivePortal.Notification"; + + private static final int SOCKET_TIMEOUT_MS = 10000; + + private String mServer; + private String mUrl; + private boolean mNotificationShown = false; + private boolean mIsCaptivePortalCheckEnabled = false; + private InternalHandler mHandler; + private IConnectivityManager mConnService; + private Context mContext; + private NetworkInfo mNetworkInfo; + private boolean mIsCaptivePortal = false; + + private static final int DETECT_PORTAL = 0; + private static final int HANDLE_CONNECT = 1; + + /** + * Activity Action: Switch to the captive portal network + * <p>Input: Nothing. + * <p>Output: Nothing. + */ + public static final String ACTION_SWITCH_TO_CAPTIVE_PORTAL + = "android.net.SWITCH_TO_CAPTIVE_PORTAL"; + + private CaptivePortalTracker(Context context, NetworkInfo info, IConnectivityManager cs) { + mContext = context; + mNetworkInfo = info; + mConnService = cs; + + HandlerThread handlerThread = new HandlerThread("CaptivePortalThread"); + handlerThread.start(); + mHandler = new InternalHandler(handlerThread.getLooper()); + mHandler.obtainMessage(DETECT_PORTAL).sendToTarget(); + + IntentFilter filter = new IntentFilter(); + filter.addAction(ACTION_SWITCH_TO_CAPTIVE_PORTAL); + filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION); + + mContext.registerReceiver(mReceiver, filter); + + mServer = Settings.Secure.getString(mContext.getContentResolver(), + Settings.Secure.CAPTIVE_PORTAL_SERVER); + if (mServer == null) mServer = DEFAULT_SERVER; + + mIsCaptivePortalCheckEnabled = Settings.Secure.getInt(mContext.getContentResolver(), + Settings.Secure.CAPTIVE_PORTAL_DETECTION_ENABLED, 1) == 1; + } + + private final BroadcastReceiver mReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + String action = intent.getAction(); + if (action.equals(ACTION_SWITCH_TO_CAPTIVE_PORTAL)) { + notifyPortalCheckComplete(); + } else if (action.equals(ConnectivityManager.CONNECTIVITY_ACTION)) { + NetworkInfo info = intent.getParcelableExtra( + ConnectivityManager.EXTRA_NETWORK_INFO); + mHandler.obtainMessage(HANDLE_CONNECT, info).sendToTarget(); + } + } + }; + + public static CaptivePortalTracker detect(Context context, NetworkInfo info, + IConnectivityManager cs) { + CaptivePortalTracker captivePortal = new CaptivePortalTracker(context, info, cs); + return captivePortal; + } + + private class InternalHandler extends Handler { + public InternalHandler(Looper looper) { + super(looper); + } + + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case DETECT_PORTAL: + InetAddress server = lookupHost(mServer); + if (server != null) { + requestRouteToHost(server); + if (isCaptivePortal(server)) { + if (DBG) log("Captive portal " + mNetworkInfo); + setNotificationVisible(true); + mIsCaptivePortal = true; + break; + } + } + notifyPortalCheckComplete(); + quit(); + break; + case HANDLE_CONNECT: + NetworkInfo info = (NetworkInfo) msg.obj; + if (info.getType() != mNetworkInfo.getType()) break; + + if (info.getState() == NetworkInfo.State.CONNECTED || + info.getState() == NetworkInfo.State.DISCONNECTED) { + setNotificationVisible(false); + } + + /* Connected to a captive portal */ + if (info.getState() == NetworkInfo.State.CONNECTED && + mIsCaptivePortal) { + launchBrowser(); + quit(); + } + break; + default: + loge("Unhandled message " + msg); + break; + } + } + + private void quit() { + mIsCaptivePortal = false; + getLooper().quit(); + mContext.unregisterReceiver(mReceiver); + } + } + + private void launchBrowser() { + Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(mUrl)); + intent.setFlags(Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT | Intent.FLAG_ACTIVITY_NEW_TASK); + mContext.startActivity(intent); + } + + private void notifyPortalCheckComplete() { + try { + mConnService.captivePortalCheckComplete(mNetworkInfo); + } catch(RemoteException e) { + e.printStackTrace(); + } + } + + private void requestRouteToHost(InetAddress server) { + try { + mConnService.requestRouteToHostAddress(mNetworkInfo.getType(), + server.getAddress()); + } catch (RemoteException e) { + e.printStackTrace(); + } + } + + /** + * Do a URL fetch on a known server to see if we get the data we expect + */ + private boolean isCaptivePortal(InetAddress server) { + HttpURLConnection urlConnection = null; + if (!mIsCaptivePortalCheckEnabled) return false; + + mUrl = "http://" + server.getHostAddress() + "/generate_204"; + try { + URL url = new URL(mUrl); + urlConnection = (HttpURLConnection) url.openConnection(); + urlConnection.setInstanceFollowRedirects(false); + urlConnection.setConnectTimeout(SOCKET_TIMEOUT_MS); + urlConnection.setReadTimeout(SOCKET_TIMEOUT_MS); + urlConnection.setUseCaches(false); + urlConnection.getInputStream(); + // we got a valid response, but not from the real google + return urlConnection.getResponseCode() != 204; + } catch (IOException e) { + if (DBG) log("Probably not a portal: exception " + e); + return false; + } finally { + if (urlConnection != null) { + urlConnection.disconnect(); + } + } + } + + private InetAddress lookupHost(String hostname) { + InetAddress inetAddress[]; + try { + inetAddress = InetAddress.getAllByName(hostname); + } catch (UnknownHostException e) { + return null; + } + + for (InetAddress a : inetAddress) { + if (a instanceof Inet4Address) return a; + } + return null; + } + + private void setNotificationVisible(boolean visible) { + // if it should be hidden and it is already hidden, then noop + if (!visible && !mNotificationShown) { + return; + } + + Resources r = Resources.getSystem(); + NotificationManager notificationManager = (NotificationManager) mContext + .getSystemService(Context.NOTIFICATION_SERVICE); + + if (visible) { + CharSequence title = r.getString(R.string.wifi_available_sign_in, 0); + CharSequence details = r.getString(R.string.wifi_available_sign_in_detailed, + mNetworkInfo.getExtraInfo()); + + Notification notification = new Notification(); + notification.when = 0; + notification.icon = com.android.internal.R.drawable.stat_notify_wifi_in_range; + notification.flags = Notification.FLAG_AUTO_CANCEL; + notification.contentIntent = PendingIntent.getBroadcast(mContext, 0, + new Intent(CaptivePortalTracker.ACTION_SWITCH_TO_CAPTIVE_PORTAL), 0); + + notification.tickerText = title; + notification.setLatestEventInfo(mContext, title, details, notification.contentIntent); + + notificationManager.notify(NOTIFICATION_ID, 1, notification); + } else { + notificationManager.cancel(NOTIFICATION_ID, 1); + } + mNotificationShown = visible; + } + + private static void log(String s) { + Log.d(TAG, s); + } + + private static void loge(String s) { + Log.e(TAG, s); + } + +} diff --git a/core/java/android/net/ConnectivityManager.java b/core/java/android/net/ConnectivityManager.java index 60bf4d6..a570473 100644 --- a/core/java/android/net/ConnectivityManager.java +++ b/core/java/android/net/ConnectivityManager.java @@ -921,4 +921,15 @@ public class ConnectivityManager { return false; } } + + /** + * {@hide} + */ + public void captivePortalCheckComplete(NetworkInfo info) { + try { + mService.captivePortalCheckComplete(info); + } catch (RemoteException e) { + } + } + } diff --git a/core/java/android/net/DummyDataStateTracker.java b/core/java/android/net/DummyDataStateTracker.java index ccd96ff..39440c2 100644 --- a/core/java/android/net/DummyDataStateTracker.java +++ b/core/java/android/net/DummyDataStateTracker.java @@ -119,6 +119,10 @@ public class DummyDataStateTracker implements NetworkStateTracker { return true; } + public void captivePortalCheckComplete() { + // not implemented + } + /** * Record the detailed state of a network, and if it is a * change from the previous state, send a notification to diff --git a/core/java/android/net/EthernetDataTracker.java b/core/java/android/net/EthernetDataTracker.java index c690430..c52aa9e 100644 --- a/core/java/android/net/EthernetDataTracker.java +++ b/core/java/android/net/EthernetDataTracker.java @@ -274,6 +274,11 @@ public class EthernetDataTracker implements NetworkStateTracker { return mLinkUp; } + @Override + public void captivePortalCheckComplete() { + // not implemented + } + /** * Turn the wireless radio off for a network. * @param turnOn {@code true} to turn the radio on, {@code false} diff --git a/core/java/android/net/IConnectivityManager.aidl b/core/java/android/net/IConnectivityManager.aidl index 3614045..056fa03 100644 --- a/core/java/android/net/IConnectivityManager.aidl +++ b/core/java/android/net/IConnectivityManager.aidl @@ -124,4 +124,6 @@ interface IConnectivityManager LegacyVpnInfo getLegacyVpnInfo(); boolean updateLockdownVpn(); + + void captivePortalCheckComplete(in NetworkInfo info); } diff --git a/core/java/android/net/MobileDataStateTracker.java b/core/java/android/net/MobileDataStateTracker.java index d59fa6a..b35d61c 100644 --- a/core/java/android/net/MobileDataStateTracker.java +++ b/core/java/android/net/MobileDataStateTracker.java @@ -381,6 +381,11 @@ public class MobileDataStateTracker implements NetworkStateTracker { return (setEnableApn(mApnType, false) != PhoneConstants.APN_REQUEST_FAILED); } + @Override + public void captivePortalCheckComplete() { + // not implemented + } + /** * Record the detailed state of a network, and if it is a * change from the previous state, send a notification to diff --git a/core/java/android/net/NetworkInfo.java b/core/java/android/net/NetworkInfo.java index 0bc6b58..0b23cb7 100644 --- a/core/java/android/net/NetworkInfo.java +++ b/core/java/android/net/NetworkInfo.java @@ -79,7 +79,9 @@ public class NetworkInfo implements Parcelable { /** Access to this network is blocked. */ BLOCKED, /** Link has poor connectivity. */ - VERIFYING_POOR_LINK + VERIFYING_POOR_LINK, + /** Checking if network is a captive portal */ + CAPTIVE_PORTAL_CHECK, } /** @@ -97,6 +99,7 @@ public class NetworkInfo implements Parcelable { stateMap.put(DetailedState.AUTHENTICATING, State.CONNECTING); stateMap.put(DetailedState.OBTAINING_IPADDR, State.CONNECTING); stateMap.put(DetailedState.VERIFYING_POOR_LINK, State.CONNECTING); + stateMap.put(DetailedState.CAPTIVE_PORTAL_CHECK, State.CONNECTING); stateMap.put(DetailedState.CONNECTED, State.CONNECTED); stateMap.put(DetailedState.SUSPENDED, State.SUSPENDED); stateMap.put(DetailedState.DISCONNECTING, State.DISCONNECTING); diff --git a/core/java/android/net/NetworkStateTracker.java b/core/java/android/net/NetworkStateTracker.java index eae89f1..0a0c1e0 100644 --- a/core/java/android/net/NetworkStateTracker.java +++ b/core/java/android/net/NetworkStateTracker.java @@ -123,6 +123,11 @@ public interface NetworkStateTracker { public boolean reconnect(); /** + * Ready to switch on to the network after captive portal check + */ + public void captivePortalCheckComplete(); + + /** * Turn the wireless radio off for a network. * @param turnOn {@code true} to turn the radio on, {@code false} */ diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index 1f6f0dd..6f43891 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -3279,13 +3279,6 @@ public final class Settings { /** - * ms delay before rechecking a connect SSID for walled garden with a http download. - * @hide - */ - public static final String WIFI_WATCHDOG_WALLED_GARDEN_INTERVAL_MS = - "wifi_watchdog_walled_garden_interval_ms"; - - /** * Number of ARP pings per check. * @hide */ @@ -3322,23 +3315,6 @@ public final class Settings { "wifi_suspend_optimizations_enabled"; /** - * Setting to turn off walled garden test on Wi-Fi. Feature is enabled by default and - * the setting needs to be set to 0 to disable it. - * @hide - */ - public static final String WIFI_WATCHDOG_WALLED_GARDEN_TEST_ENABLED = - "wifi_watchdog_walled_garden_test_enabled"; - - /** - * The URL used for walled garden check upon a new conection. WifiWatchdogService - * fetches the URL and checks to see if {@link #WIFI_WATCHDOG_WALLED_GARDEN_PATTERN} - * is not part of the title string to notify the user on the presence of a walled garden. - * @hide - */ - public static final String WIFI_WATCHDOG_WALLED_GARDEN_URL = - "wifi_watchdog_walled_garden_url"; - - /** * The maximum number of times we will retry a connection to an access * point for which we have failed in acquiring an IP address from DHCP. * A value of N means that we will make N+1 connection attempts in all. @@ -3362,6 +3338,21 @@ public final class Settings { public static final String WIFI_P2P_DEVICE_NAME = "wifi_p2p_device_name"; /** + * Setting to turn off captive portal detection. Feature is enabled by default and + * the setting needs to be set to 0 to disable it. + * @hide + */ + public static final String CAPTIVE_PORTAL_DETECTION_ENABLED = + "captive_portal_detection_enabled"; + + /** + * The server used for captive portal detection upon a new conection. A 204 response + * code from the server is used for validation. + * @hide + */ + public static final String CAPTIVE_PORTAL_SERVER = "captive_portal_server"; + + /** * Maximum amount of time in milliseconds to hold a wakelock while waiting for mobile * data connectivity to be established after a disconnect from Wi-Fi. */ diff --git a/services/java/com/android/server/ConnectivityService.java b/services/java/com/android/server/ConnectivityService.java index 776a1a4..8a1ac10 100644 --- a/services/java/com/android/server/ConnectivityService.java +++ b/services/java/com/android/server/ConnectivityService.java @@ -42,6 +42,7 @@ import android.content.IntentFilter; import android.content.pm.PackageManager; import android.content.res.Resources; import android.database.ContentObserver; +import android.net.CaptivePortalTracker; import android.net.ConnectivityManager; import android.net.DummyDataStateTracker; import android.net.EthernetDataTracker; @@ -166,6 +167,9 @@ public class ConnectivityService extends IConnectivityManager.Stub { */ private NetworkStateTracker mNetTrackers[]; + /* Handles captive portal check on a network */ + private CaptivePortalTracker mCaptivePortalTracker; + /** * The link properties that define the current links */ @@ -1363,8 +1367,10 @@ public class ConnectivityService extends IConnectivityManager.Stub { return false; } NetworkStateTracker tracker = mNetTrackers[networkType]; + DetailedState netState = tracker.getNetworkInfo().getDetailedState(); - if (tracker == null || !tracker.getNetworkInfo().isConnected() || + if (tracker == null || (netState != DetailedState.CONNECTED && + netState != DetailedState.CAPTIVE_PORTAL_CHECK) || tracker.isTeardownRequested()) { if (VDBG) { log("requestRouteToHostAddress on down network " + @@ -1966,32 +1972,29 @@ public class ConnectivityService extends IConnectivityManager.Stub { } }; + private boolean isNewNetTypePreferredOverCurrentNetType(int type) { + if ((type != mNetworkPreference && + mNetConfigs[mActiveDefaultNetwork].priority > + mNetConfigs[type].priority) || + mNetworkPreference == mActiveDefaultNetwork) return false; + return true; + } + private void handleConnect(NetworkInfo info) { - final int type = info.getType(); + final int newNetType = info.getType(); - setupDataActivityTracking(type); + setupDataActivityTracking(newNetType); // snapshot isFailover, because sendConnectedBroadcast() resets it boolean isFailover = info.isFailover(); - final NetworkStateTracker thisNet = mNetTrackers[type]; + final NetworkStateTracker thisNet = mNetTrackers[newNetType]; final String thisIface = thisNet.getLinkProperties().getInterfaceName(); // if this is a default net and other default is running // kill the one not preferred - if (mNetConfigs[type].isDefault()) { - if (mActiveDefaultNetwork != -1 && mActiveDefaultNetwork != type) { - if ((type != mNetworkPreference && - mNetConfigs[mActiveDefaultNetwork].priority > - mNetConfigs[type].priority) || - mNetworkPreference == mActiveDefaultNetwork) { - // don't accept this one - if (VDBG) { - log("Not broadcasting CONNECT_ACTION " + - "to torn down network " + info.getTypeName()); - } - teardown(thisNet); - return; - } else { + if (mNetConfigs[newNetType].isDefault()) { + if (mActiveDefaultNetwork != -1 && mActiveDefaultNetwork != newNetType) { + if (isNewNetTypePreferredOverCurrentNetType(newNetType)) { // tear down the other NetworkStateTracker otherNet = mNetTrackers[mActiveDefaultNetwork]; @@ -2004,6 +2007,14 @@ public class ConnectivityService extends IConnectivityManager.Stub { teardown(thisNet); return; } + } else { + // don't accept this one + if (VDBG) { + log("Not broadcasting CONNECT_ACTION " + + "to torn down network " + info.getTypeName()); + } + teardown(thisNet); + return; } } synchronized (ConnectivityService.this) { @@ -2017,7 +2028,7 @@ public class ConnectivityService extends IConnectivityManager.Stub { 1000); } } - mActiveDefaultNetwork = type; + mActiveDefaultNetwork = newNetType; // this will cause us to come up initially as unconnected and switching // to connected after our normal pause unless somebody reports us as reall // disconnected @@ -2029,19 +2040,47 @@ public class ConnectivityService extends IConnectivityManager.Stub { } thisNet.setTeardownRequested(false); updateNetworkSettings(thisNet); - handleConnectivityChange(type, false); + handleConnectivityChange(newNetType, false); sendConnectedBroadcastDelayed(info, getConnectivityChangeDelay()); // notify battery stats service about this network if (thisIface != null) { try { - BatteryStatsService.getService().noteNetworkInterfaceType(thisIface, type); + BatteryStatsService.getService().noteNetworkInterfaceType(thisIface, newNetType); } catch (RemoteException e) { // ignored; service lives in system_server } } } + private void handleCaptivePortalTrackerCheck(NetworkInfo info) { + if (DBG) log("Captive portal check " + info); + int type = info.getType(); + final NetworkStateTracker thisNet = mNetTrackers[type]; + if (mNetConfigs[type].isDefault()) { + if (mActiveDefaultNetwork != -1 && mActiveDefaultNetwork != type) { + if (isNewNetTypePreferredOverCurrentNetType(type)) { + if (DBG) log("Captive check on " + info.getTypeName()); + mCaptivePortalTracker = CaptivePortalTracker.detect(mContext, info, + ConnectivityService.this); + return; + } else { + if (DBG) log("Tear down low priority net " + info.getTypeName()); + teardown(thisNet); + return; + } + } + } + + thisNet.captivePortalCheckComplete(); + } + + /** @hide */ + public void captivePortalCheckComplete(NetworkInfo info) { + mNetTrackers[info.getType()].captivePortalCheckComplete(); + mCaptivePortalTracker = null; + } + /** * Setup data activity tracking for the given network interface. * @@ -2630,6 +2669,9 @@ public class ConnectivityService extends IConnectivityManager.Stub { if (info.getDetailedState() == NetworkInfo.DetailedState.FAILED) { handleConnectionFailure(info); + } else if (info.getDetailedState() == + DetailedState.CAPTIVE_PORTAL_CHECK) { + handleCaptivePortalTrackerCheck(info); } else if (state == NetworkInfo.State.DISCONNECTED) { handleDisconnect(info); } else if (state == NetworkInfo.State.SUSPENDED) { diff --git a/services/java/com/android/server/WifiService.java b/services/java/com/android/server/WifiService.java index f483576..84d670e 100644 --- a/services/java/com/android/server/WifiService.java +++ b/services/java/com/android/server/WifiService.java @@ -412,6 +412,7 @@ public class WifiService extends IWifiManager.Stub { switch(mNetworkInfo.getDetailedState()) { case CONNECTED: case DISCONNECTED: + case CAPTIVE_PORTAL_CHECK: evaluateTrafficStatsPolling(); resetNotification(); break; @@ -606,6 +607,12 @@ public class WifiService extends IWifiManager.Stub { "WifiService"); } + private void enforceConnectivityInternalPermission() { + mContext.enforceCallingOrSelfPermission( + android.Manifest.permission.CONNECTIVITY_INTERNAL, + "ConnectivityService"); + } + /** * see {@link android.net.wifi.WifiManager#setWifiEnabled(boolean)} * @param enable {@code true} to enable, {@code false} to disable. @@ -910,7 +917,7 @@ public class WifiService extends IWifiManager.Stub { * */ public void startWifi() { - enforceChangePermission(); + enforceConnectivityInternalPermission(); /* TODO: may be add permissions for access only to connectivity service * TODO: if a start issued, keep wifi alive until a stop issued irrespective * of WifiLock & device idle status unless wifi enabled status is toggled @@ -920,20 +927,24 @@ public class WifiService extends IWifiManager.Stub { mWifiStateMachine.reconnectCommand(); } + public void captivePortalCheckComplete() { + enforceConnectivityInternalPermission(); + mWifiStateMachine.captivePortalCheckComplete(); + } + /** * see {@link android.net.wifi.WifiManager#stopWifi} * */ public void stopWifi() { - enforceChangePermission(); - /* TODO: may be add permissions for access only to connectivity service + enforceConnectivityInternalPermission(); + /* * TODO: if a stop is issued, wifi is brought up only by startWifi * unless wifi enabled status is toggled */ mWifiStateMachine.setDriverStart(false, mEmergencyCallbackMode); } - /** * see {@link android.net.wifi.WifiManager#addToBlacklist} * diff --git a/wifi/java/android/net/wifi/IWifiManager.aidl b/wifi/java/android/net/wifi/IWifiManager.aidl index 6b08074..55de065 100644 --- a/wifi/java/android/net/wifi/IWifiManager.aidl +++ b/wifi/java/android/net/wifi/IWifiManager.aidl @@ -106,5 +106,7 @@ interface IWifiManager Messenger getWifiStateMachineMessenger(); String getConfigFile(); + + void captivePortalCheckComplete(); } diff --git a/wifi/java/android/net/wifi/WifiManager.java b/wifi/java/android/net/wifi/WifiManager.java index 284bee8..aa59158 100644 --- a/wifi/java/android/net/wifi/WifiManager.java +++ b/wifi/java/android/net/wifi/WifiManager.java @@ -1971,4 +1971,11 @@ public class WifiManager { return false; } } + + /** @hide */ + public void captivePortalCheckComplete() { + try { + mService.captivePortalCheckComplete(); + } catch (RemoteException e) {} + } } diff --git a/wifi/java/android/net/wifi/WifiStateMachine.java b/wifi/java/android/net/wifi/WifiStateMachine.java index a5322fa..b4456b9 100644 --- a/wifi/java/android/net/wifi/WifiStateMachine.java +++ b/wifi/java/android/net/wifi/WifiStateMachine.java @@ -256,6 +256,8 @@ public class WifiStateMachine extends StateMachine { static final int CMD_DELAYED_STOP_DRIVER = BASE + 18; /* A delayed message sent to start driver when it fail to come up */ static final int CMD_DRIVER_START_TIMED_OUT = BASE + 19; + /* Ready to switch to network as default */ + static final int CMD_CAPTIVE_CHECK_COMPLETE = BASE + 20; /* Start the soft access point */ static final int CMD_START_AP = BASE + 21; @@ -459,6 +461,8 @@ public class WifiStateMachine extends StateMachine { private State mObtainingIpState = new ObtainingIpState(); /* Waiting for link quality verification to be complete */ private State mVerifyingLinkState = new VerifyingLinkState(); + /* Waiting for captive portal check to be complete */ + private State mCaptivePortalCheckState = new CaptivePortalCheckState(); /* Connected with IP addr */ private State mConnectedState = new ConnectedState(); /* disconnect issued, waiting for network disconnect confirmation */ @@ -695,6 +699,7 @@ public class WifiStateMachine extends StateMachine { addState(mL2ConnectedState, mConnectModeState); addState(mObtainingIpState, mL2ConnectedState); addState(mVerifyingLinkState, mL2ConnectedState); + addState(mCaptivePortalCheckState, mL2ConnectedState); addState(mConnectedState, mL2ConnectedState); addState(mDisconnectingState, mConnectModeState); addState(mDisconnectedState, mConnectModeState); @@ -865,6 +870,10 @@ public class WifiStateMachine extends StateMachine { } } + public void captivePortalCheckComplete() { + sendMessage(obtainMessage(CMD_CAPTIVE_CHECK_COMPLETE)); + } + /** * TODO: doc */ @@ -1616,7 +1625,7 @@ public class WifiStateMachine extends StateMachine { } if (state != mNetworkInfo.getDetailedState()) { - mNetworkInfo.setDetailedState(state, null, null); + mNetworkInfo.setDetailedState(state, null, mWifiInfo.getSSID()); } } @@ -3253,6 +3262,26 @@ public class WifiStateMachine extends StateMachine { //stay here break; case WifiWatchdogStateMachine.GOOD_LINK_DETECTED: + transitionTo(mCaptivePortalCheckState); + break; + default: + return NOT_HANDLED; + } + return HANDLED; + } + } + + class CaptivePortalCheckState extends State { + @Override + public void enter() { + setNetworkDetailedState(DetailedState.CAPTIVE_PORTAL_CHECK); + mWifiConfigStore.updateStatus(mLastNetworkId, DetailedState.CAPTIVE_PORTAL_CHECK); + sendNetworkStateChangeBroadcast(mLastBssid); + } + @Override + public boolean processMessage(Message message) { + switch (message.what) { + case CMD_CAPTIVE_CHECK_COMPLETE: try { mNwService.enableIpv6(mInterfaceName); } catch (RemoteException re) { @@ -3260,7 +3289,6 @@ public class WifiStateMachine extends StateMachine { } catch (IllegalStateException e) { loge("Failed to enable IPv6: " + e); } - setNetworkDetailedState(DetailedState.CONNECTED); mWifiConfigStore.updateStatus(mLastNetworkId, DetailedState.CONNECTED); sendNetworkStateChangeBroadcast(mLastBssid); diff --git a/wifi/java/android/net/wifi/WifiStateTracker.java b/wifi/java/android/net/wifi/WifiStateTracker.java index bfb91e2..a5a2469 100644 --- a/wifi/java/android/net/wifi/WifiStateTracker.java +++ b/wifi/java/android/net/wifi/WifiStateTracker.java @@ -23,6 +23,7 @@ import android.content.IntentFilter; import android.net.LinkCapabilities; import android.net.LinkProperties; import android.net.NetworkInfo; +import android.net.NetworkInfo.DetailedState; import android.net.NetworkStateTracker; import android.net.wifi.p2p.WifiP2pManager; import android.os.Handler; @@ -113,6 +114,14 @@ public class WifiStateTracker implements NetworkStateTracker { } /** + * Captive check is complete, switch to network + */ + @Override + public void captivePortalCheckComplete() { + mWifiManager.captivePortalCheckComplete(); + } + + /** * Turn the wireless radio off for a network. * @param turnOn {@code true} to turn the radio on, {@code false} */ @@ -235,9 +244,10 @@ public class WifiStateTracker implements NetworkStateTracker { mLinkCapabilities = new LinkCapabilities(); } // don't want to send redundent state messages - // TODO can this be fixed in WifiStateMachine? + // but send portal check detailed state notice NetworkInfo.State state = mNetworkInfo.getState(); - if (mLastState == state) { + if (mLastState == state && + mNetworkInfo.getDetailedState() != DetailedState.CAPTIVE_PORTAL_CHECK) { return; } else { mLastState = state; diff --git a/wifi/java/android/net/wifi/WifiWatchdogStateMachine.java b/wifi/java/android/net/wifi/WifiWatchdogStateMachine.java index 29a53b6..7fa6aac 100644 --- a/wifi/java/android/net/wifi/WifiWatchdogStateMachine.java +++ b/wifi/java/android/net/wifi/WifiWatchdogStateMachine.java @@ -16,20 +16,15 @@ package android.net.wifi; -import android.app.Notification; -import android.app.NotificationManager; -import android.app.PendingIntent; import android.content.BroadcastReceiver; import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; -import android.content.res.Resources; import android.database.ContentObserver; import android.net.ConnectivityManager; import android.net.LinkProperties; import android.net.NetworkInfo; -import android.net.Uri; import android.net.wifi.RssiPacketCountInfo; import android.os.Message; import android.os.SystemClock; @@ -44,10 +39,7 @@ import com.android.internal.util.Protocol; import com.android.internal.util.State; import com.android.internal.util.StateMachine; -import java.io.IOException; import java.io.PrintWriter; -import java.net.HttpURLConnection; -import java.net.URL; import java.text.DecimalFormat; /** @@ -100,8 +92,7 @@ public class WifiWatchdogStateMachine extends StateMachine { private static final int EVENT_SCREEN_OFF = BASE + 9; /* Internal messages */ - private static final int CMD_DELAYED_WALLED_GARDEN_CHECK = BASE + 11; - private static final int CMD_RSSI_FETCH = BASE + 12; + private static final int CMD_RSSI_FETCH = BASE + 11; /* Notifications from/to WifiStateMachine */ static final int POOR_LINK_DETECTED = BASE + 21; @@ -266,27 +257,6 @@ public class WifiWatchdogStateMachine extends StateMachine { new MaxAvoidTime( 0 * 60000, -55 ), }; - - private static final String WALLED_GARDEN_NOTIFICATION_ID = "WifiWatchdog.walledgarden"; - - private static final long DEFAULT_WALLED_GARDEN_INTERVAL_MS = 30 * 60 * 1000; - - /** - * See http://go/clientsdns for usage approval - */ - private static final String DEFAULT_WALLED_GARDEN_URL = - "http://clients3.google.com/generate_204"; - private static final int WALLED_GARDEN_SOCKET_TIMEOUT_MS = 10000; - - /** - * Some carrier apps might have support captive portal handling. Add some - * delay to allow app authentication to be done before our test. TODO: This - * should go away once we provide an API to apps to disable walled garden - * test for certain SSIDs - */ - private static final int WALLED_GARDEN_START_DELAY_MS = 3000; - - /* Framework related */ private Context mContext; private ContentResolver mContentResolver; @@ -300,13 +270,6 @@ public class WifiWatchdogStateMachine extends StateMachine { /* System settingss related */ private static boolean sWifiOnly = false; private boolean mPoorNetworkDetectionEnabled; - private long mWalledGardenIntervalMs; - private boolean mWalledGardenTestEnabled; - private String mWalledGardenUrl; - - /* Wall garden detection related */ - private long mLastWalledGardenCheckTime = 0; - private boolean mWalledGardenNotificationShown; /* Poor link detection related */ private LruCache<String, BssidStatistics> mBssidCache = @@ -325,7 +288,6 @@ public class WifiWatchdogStateMachine extends StateMachine { private NotConnectedState mNotConnectedState = new NotConnectedState(); private VerifyingLinkState mVerifyingLinkState = new VerifyingLinkState(); private ConnectedState mConnectedState = new ConnectedState(); - private WalledGardenCheckState mWalledGardenCheckState = new WalledGardenCheckState(); private OnlineWatchState mOnlineWatchState = new OnlineWatchState(); private LinkMonitoringState mLinkMonitoringState = new LinkMonitoringState(); private OnlineState mOnlineState = new OnlineState(); @@ -359,7 +321,6 @@ public class WifiWatchdogStateMachine extends StateMachine { addState(mNotConnectedState, mWatchdogEnabledState); addState(mVerifyingLinkState, mWatchdogEnabledState); addState(mConnectedState, mWatchdogEnabledState); - addState(mWalledGardenCheckState, mConnectedState); addState(mOnlineWatchState, mConnectedState); addState(mLinkMonitoringState, mConnectedState); addState(mOnlineState, mConnectedState); @@ -379,13 +340,12 @@ public class WifiWatchdogStateMachine extends StateMachine { Context.CONNECTIVITY_SERVICE); sWifiOnly = (cm.isNetworkSupported(ConnectivityManager.TYPE_MOBILE) == false); - // Watchdog is always enabled. Poor network detection & walled garden detection - // can individually be turned on/off + // Watchdog is always enabled. Poor network detection can be seperately turned on/off // TODO: Remove this setting & clean up state machine since we always have // watchdog in an enabled state putSettingsBoolean(contentResolver, Settings.Secure.WIFI_WATCHDOG_ON, true); - // disable poor network avoidance, but keep watchdog active for walled garden detection + // disable poor network avoidance if (sWifiOnly) { logd("Disabling poor network avoidance for wi-fi only device"); putSettingsBoolean(contentResolver, @@ -458,44 +418,8 @@ public class WifiWatchdogStateMachine extends StateMachine { }; mContext.getContentResolver().registerContentObserver( - Settings.Secure.getUriFor(Settings.Secure.WIFI_WATCHDOG_WALLED_GARDEN_INTERVAL_MS), - false, contentObserver); - mContext.getContentResolver().registerContentObserver( Settings.Secure.getUriFor(Settings.Secure.WIFI_WATCHDOG_POOR_NETWORK_TEST_ENABLED), false, contentObserver); - mContext.getContentResolver().registerContentObserver( - Settings.Secure.getUriFor(Settings.Secure.WIFI_WATCHDOG_WALLED_GARDEN_TEST_ENABLED), - false, contentObserver); - mContext.getContentResolver().registerContentObserver( - Settings.Secure.getUriFor(Settings.Secure.WIFI_WATCHDOG_WALLED_GARDEN_URL), - false, contentObserver); - } - - /** - * DNS based detection techniques do not work at all hotspots. The one sure - * way to check a walled garden is to see if a URL fetch on a known address - * fetches the data we expect - */ - private boolean isWalledGardenConnection() { - HttpURLConnection urlConnection = null; - try { - URL url = new URL(mWalledGardenUrl); - urlConnection = (HttpURLConnection) url.openConnection(); - urlConnection.setInstanceFollowRedirects(false); - urlConnection.setConnectTimeout(WALLED_GARDEN_SOCKET_TIMEOUT_MS); - urlConnection.setReadTimeout(WALLED_GARDEN_SOCKET_TIMEOUT_MS); - urlConnection.setUseCaches(false); - urlConnection.getInputStream(); - // we got a valid response, but not from the real google - return urlConnection.getResponseCode() != 204; - } catch (IOException e) { - if (DBG) logd("Walled garden check - probably not a portal: exception " + e); - return false; - } finally { - if (urlConnection != null) { - urlConnection.disconnect(); - } - } } public void dump(PrintWriter pw) { @@ -504,10 +428,7 @@ public class WifiWatchdogStateMachine extends StateMachine { pw.println("mWifiInfo: [" + mWifiInfo + "]"); pw.println("mLinkProperties: [" + mLinkProperties + "]"); pw.println("mCurrentSignalLevel: [" + mCurrentSignalLevel + "]"); - pw.println("mWalledGardenIntervalMs: [" + mWalledGardenIntervalMs + "]"); pw.println("mPoorNetworkDetectionEnabled: [" + mPoorNetworkDetectionEnabled + "]"); - pw.println("mWalledGardenTestEnabled: [" + mWalledGardenTestEnabled + "]"); - pw.println("mWalledGardenUrl: [" + mWalledGardenUrl + "]"); } private boolean isWatchdogEnabled() { @@ -521,47 +442,6 @@ public class WifiWatchdogStateMachine extends StateMachine { mPoorNetworkDetectionEnabled = getSettingsBoolean(mContentResolver, Settings.Secure.WIFI_WATCHDOG_POOR_NETWORK_TEST_ENABLED, true); - mWalledGardenTestEnabled = getSettingsBoolean(mContentResolver, - Settings.Secure.WIFI_WATCHDOG_WALLED_GARDEN_TEST_ENABLED, true); - mWalledGardenUrl = getSettingsStr(mContentResolver, - Settings.Secure.WIFI_WATCHDOG_WALLED_GARDEN_URL, - DEFAULT_WALLED_GARDEN_URL); - mWalledGardenIntervalMs = Secure.getLong(mContentResolver, - Secure.WIFI_WATCHDOG_WALLED_GARDEN_INTERVAL_MS, - DEFAULT_WALLED_GARDEN_INTERVAL_MS); - } - - private void setWalledGardenNotificationVisible(boolean visible) { - // if it should be hidden and it is already hidden, then noop - if (!visible && !mWalledGardenNotificationShown) { - return; - } - - Resources r = Resources.getSystem(); - NotificationManager notificationManager = (NotificationManager) mContext - .getSystemService(Context.NOTIFICATION_SERVICE); - - if (visible) { - Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(mWalledGardenUrl)); - intent.setFlags(Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT | Intent.FLAG_ACTIVITY_NEW_TASK); - - CharSequence title = r.getString(R.string.wifi_available_sign_in, 0); - CharSequence details = r.getString(R.string.wifi_available_sign_in_detailed, - mWifiInfo.getSSID()); - - Notification notification = new Notification(); - notification.when = 0; - notification.icon = com.android.internal.R.drawable.stat_notify_wifi_in_range; - notification.flags = Notification.FLAG_AUTO_CANCEL; - notification.contentIntent = PendingIntent.getActivity(mContext, 0, intent, 0); - notification.tickerText = title; - notification.setLatestEventInfo(mContext, title, details, notification.contentIntent); - - notificationManager.notify(WALLED_GARDEN_NOTIFICATION_ID, 1, notification); - } else { - notificationManager.cancel(WALLED_GARDEN_NOTIFICATION_ID, 1); - } - mWalledGardenNotificationShown = visible; } /** @@ -587,7 +467,6 @@ public class WifiWatchdogStateMachine extends StateMachine { case EVENT_NETWORK_STATE_CHANGE: case EVENT_SUPPLICANT_STATE_CHANGE: case EVENT_BSSID_CHANGE: - case CMD_DELAYED_WALLED_GARDEN_CHECK: case CMD_RSSI_FETCH: case WifiManager.RSSI_PKTCNT_FETCH_SUCCEEDED: case WifiManager.RSSI_PKTCNT_FETCH_FAILED: @@ -685,11 +564,7 @@ public class WifiWatchdogStateMachine extends StateMachine { } break; case CONNECTED: - if (shouldCheckWalledGarden()) { - transitionTo(mWalledGardenCheckState); - } else { - transitionTo(mOnlineWatchState); - } + transitionTo(mOnlineWatchState); break; default: transitionTo(mNotConnectedState); @@ -716,7 +591,6 @@ public class WifiWatchdogStateMachine extends StateMachine { return NOT_HANDLED; } - setWalledGardenNotificationVisible(false); return HANDLED; } } @@ -834,38 +708,6 @@ public class WifiWatchdogStateMachine extends StateMachine { } /** - * Checking for wall garden. - */ - class WalledGardenCheckState extends State { - private int mWalledGardenToken = 0; - @Override - public void enter() { - if (DBG) logd(getName()); - sendMessageDelayed(obtainMessage(CMD_DELAYED_WALLED_GARDEN_CHECK, - ++mWalledGardenToken, 0), WALLED_GARDEN_START_DELAY_MS); - } - - @Override - public boolean processMessage(Message msg) { - switch (msg.what) { - case CMD_DELAYED_WALLED_GARDEN_CHECK: - if (msg.arg1 == mWalledGardenToken) { - mLastWalledGardenCheckTime = SystemClock.elapsedRealtime(); - if (isWalledGardenConnection()) { - if (DBG) logd("Walled garden detected"); - setWalledGardenNotificationVisible(true); - } - transitionTo(mOnlineWatchState); - } - break; - default: - return NOT_HANDLED; - } - return HANDLED; - } - } - - /** * RSSI is high enough and don't need link monitoring. */ class OnlineWatchState extends State { @@ -1037,22 +879,6 @@ public class WifiWatchdogStateMachine extends StateMachine { } } - private boolean shouldCheckWalledGarden() { - if (!mWalledGardenTestEnabled) { - if (DBG) logd("Skipping walled garden check - disabled"); - return false; - } - - long waitTime = (mWalledGardenIntervalMs + mLastWalledGardenCheckTime) - - SystemClock.elapsedRealtime(); - - if (mLastWalledGardenCheckTime != 0 && waitTime > 0) { - if (DBG) logd("Skipping walled garden check - wait " + waitTime + " ms."); - return false; - } - return true; - } - private void updateCurrentBssid(String bssid) { if (DBG) logd("Update current BSSID to " + (bssid != null ? bssid : "null")); |