diff options
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")); |