Enable network validations and add app to handle captive portal login.

Network validation prevents networks claiming to provide internet connectivity
from becoming the default network in cases where internet connectivity is not
found to actually exist.
If a captive portal is encountered the appropriate broadcasts and notifications
are surfaced to allow apps to handle signing in.  If no app handles signing in,
my system app will handle it.

Bug:15409233
Bug:15409354

Change-Id: Ie240d7eac4bdbab8cc7578782bd72d8b26de7951
diff --git a/packages/CaptivePortalLogin/Android.mk b/packages/CaptivePortalLogin/Android.mk
new file mode 100644
index 0000000..576debc
--- /dev/null
+++ b/packages/CaptivePortalLogin/Android.mk
@@ -0,0 +1,11 @@
+LOCAL_PATH:= $(call my-dir)
+include $(CLEAR_VARS)
+
+LOCAL_MODULE_TAGS := optional
+
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+LOCAL_PACKAGE_NAME := CaptivePortalLogin
+LOCAL_CERTIFICATE := platform
+
+include $(BUILD_PACKAGE)
diff --git a/packages/CaptivePortalLogin/AndroidManifest.xml b/packages/CaptivePortalLogin/AndroidManifest.xml
new file mode 100644
index 0000000..5f78afe
--- /dev/null
+++ b/packages/CaptivePortalLogin/AndroidManifest.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+ * Copyright (C) 2014 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.
+ */
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.android.captiveportallogin" >
+
+    <uses-permission android:name="android.permission.INTERNET" />
+
+    <application android:label="@string/app_name" >
+        <activity
+            android:name="com.android.captiveportallogin.CaptivePortalLoginActivity"
+            android:label="@string/action_bar_label"
+            android:theme="@android:style/Theme.Holo" >
+            <intent-filter>
+                <action android:name="android.intent.action.ACTION_SEND"/>
+                <category android:name="android.intent.category.DEFAULT"/>
+                <data android:mimeType="text/plain"/>
+            </intent-filter>
+        </activity>
+    </application>
+</manifest>
diff --git a/packages/CaptivePortalLogin/res/layout/activity_captive_portal_login.xml b/packages/CaptivePortalLogin/res/layout/activity_captive_portal_login.xml
new file mode 100644
index 0000000..d8f2928
--- /dev/null
+++ b/packages/CaptivePortalLogin/res/layout/activity_captive_portal_login.xml
@@ -0,0 +1,20 @@
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:id="@+id/container"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    tools:context="com.android.captiveportallogin.CaptivePortalLoginActivity"
+    tools:ignore="MergeRootFrame">
+    <RelativeLayout
+    android:layout_width="match_parent"
+    android:layout_height="match_parent" >
+
+    <WebView
+        android:id="@+id/webview"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:layout_alignParentBottom="false"
+        android:layout_alignParentRight="false" />
+
+</RelativeLayout>
+</FrameLayout>
diff --git a/packages/CaptivePortalLogin/res/menu/captive_portal_login.xml b/packages/CaptivePortalLogin/res/menu/captive_portal_login.xml
new file mode 100644
index 0000000..1a88c5c
--- /dev/null
+++ b/packages/CaptivePortalLogin/res/menu/captive_portal_login.xml
@@ -0,0 +1,15 @@
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    tools:context="com.android.captiveportallogin.CaptivePortalLoginActivity" >
+    <item
+        android:id="@+id/action_do_not_use_network"
+        android:orderInCategory="100"
+        android:showAsAction="never"
+        android:title="@string/action_do_not_use_network"/>
+    <item
+        android:id="@+id/action_use_network"
+        android:orderInCategory="200"
+        android:showAsAction="never"
+        android:title="@string/action_use_network"/>
+
+</menu>
diff --git a/packages/CaptivePortalLogin/res/values/dimens.xml b/packages/CaptivePortalLogin/res/values/dimens.xml
new file mode 100644
index 0000000..55c1e59
--- /dev/null
+++ b/packages/CaptivePortalLogin/res/values/dimens.xml
@@ -0,0 +1,7 @@
+<resources>
+
+    <!-- Default screen margins, per the Android Design guidelines. -->
+    <dimen name="activity_horizontal_margin">16dp</dimen>
+    <dimen name="activity_vertical_margin">16dp</dimen>
+
+</resources>
diff --git a/packages/CaptivePortalLogin/res/values/strings.xml b/packages/CaptivePortalLogin/res/values/strings.xml
new file mode 100644
index 0000000..1b0f0a4
--- /dev/null
+++ b/packages/CaptivePortalLogin/res/values/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+    <string name="app_name">CaptivePortalLogin</string>
+    <string name="action_use_network">Use this network as is</string>
+    <string name="action_do_not_use_network">Do not use this network</string>
+    <string name="action_bar_label">Sign-in to network</string>
+
+</resources>
diff --git a/packages/CaptivePortalLogin/res/values/styles.xml b/packages/CaptivePortalLogin/res/values/styles.xml
new file mode 100644
index 0000000..6ce89c7
--- /dev/null
+++ b/packages/CaptivePortalLogin/res/values/styles.xml
@@ -0,0 +1,20 @@
+<resources>
+
+    <!--
+        Base application theme, dependent on API level. This theme is replaced
+        by AppBaseTheme from res/values-vXX/styles.xml on newer devices.
+    -->
+    <style name="AppBaseTheme" parent="android:Theme.Light">
+        <!--
+            Theme customizations available in newer API levels can go in
+            res/values-vXX/styles.xml, while customizations related to
+            backward-compatibility can go here.
+        -->
+    </style>
+
+    <!-- Application theme. -->
+    <style name="AppTheme" parent="AppBaseTheme">
+        <!-- All customizations that are NOT specific to a particular API-level can go here. -->
+    </style>
+
+</resources>
diff --git a/packages/CaptivePortalLogin/src/com/android/captiveportallogin/CaptivePortalLoginActivity.java b/packages/CaptivePortalLogin/src/com/android/captiveportallogin/CaptivePortalLoginActivity.java
new file mode 100644
index 0000000..2c1db02
--- /dev/null
+++ b/packages/CaptivePortalLogin/src/com/android/captiveportallogin/CaptivePortalLoginActivity.java
@@ -0,0 +1,160 @@
+/*
+ * Copyright (C) 2014 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 com.android.captiveportallogin;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.graphics.Bitmap;
+import android.net.ConnectivityManager;
+import android.net.Network;
+import android.os.Bundle;
+import android.provider.Settings;
+import android.provider.Settings.Global;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.Window;
+import android.webkit.WebChromeClient;
+import android.webkit.WebSettings;
+import android.webkit.WebView;
+import android.webkit.WebViewClient;
+
+import java.io.IOException;
+import java.net.HttpURLConnection;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.lang.InterruptedException;
+
+public class CaptivePortalLoginActivity extends Activity {
+    private static final String DEFAULT_SERVER = "clients3.google.com";
+    private static final int SOCKET_TIMEOUT_MS = 10000;
+
+    // Keep this in sync with NetworkMonitor.
+    // Intent broadcast to ConnectivityService indicating sign-in is complete.
+    // Extras:
+    //     EXTRA_TEXT       = netId
+    //     LOGGED_IN_RESULT = "1" if we should use network, "0" if not.
+    private static final String ACTION_CAPTIVE_PORTAL_LOGGED_IN =
+            "android.net.netmon.captive_portal_logged_in";
+    private static final String LOGGED_IN_RESULT = "result";
+
+    private URL mURL;
+    private int mNetId;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        String server = Settings.Global.getString(getContentResolver(), "captive_portal_server");
+        if (server == null) server = DEFAULT_SERVER;
+        try {
+            mURL = new URL("http://" + server + "/generate_204");
+        } catch (MalformedURLException e) {
+            done(true);
+        }
+
+        requestWindowFeature(Window.FEATURE_PROGRESS);
+        setContentView(R.layout.activity_captive_portal_login);
+
+        getActionBar().setDisplayShowHomeEnabled(false);
+
+        mNetId = Integer.parseInt(getIntent().getStringExtra(Intent.EXTRA_TEXT));
+        ConnectivityManager.setProcessDefaultNetwork(new Network(mNetId));
+
+        WebView myWebView = (WebView) findViewById(R.id.webview);
+        WebSettings webSettings = myWebView.getSettings();
+        webSettings.setJavaScriptEnabled(true);
+        myWebView.setWebViewClient(new MyWebViewClient());
+        myWebView.setWebChromeClient(new MyWebChromeClient());
+        myWebView.loadUrl(mURL.toString());
+    }
+
+    private void done(boolean use_network) {
+        Intent intent = new Intent(ACTION_CAPTIVE_PORTAL_LOGGED_IN);
+        intent.putExtra(Intent.EXTRA_TEXT, String.valueOf(mNetId));
+        intent.putExtra(LOGGED_IN_RESULT, use_network ? "1" : "0");
+        sendBroadcast(intent);
+        finish();
+    }
+
+    @Override
+    public boolean onCreateOptionsMenu(Menu menu) {
+        getMenuInflater().inflate(R.menu.captive_portal_login, menu);
+        return true;
+    }
+
+    @Override
+    public boolean onOptionsItemSelected(MenuItem item) {
+        int id = item.getItemId();
+        if (id == R.id.action_use_network) {
+            done(true);
+            return true;
+        }
+        if (id == R.id.action_do_not_use_network) {
+            done(false);
+            return true;
+        }
+        return super.onOptionsItemSelected(item);
+    }
+
+    private void testForCaptivePortal() {
+        new Thread(new Runnable() {
+            public void run() {
+                // Give time for captive portal to open.
+                try {
+                    Thread.sleep(1000);
+                } catch (InterruptedException e) {
+                }
+                HttpURLConnection urlConnection = null;
+                int httpResponseCode = 500;
+                try {
+                    urlConnection = (HttpURLConnection) mURL.openConnection();
+                    urlConnection.setInstanceFollowRedirects(false);
+                    urlConnection.setConnectTimeout(SOCKET_TIMEOUT_MS);
+                    urlConnection.setReadTimeout(SOCKET_TIMEOUT_MS);
+                    urlConnection.setUseCaches(false);
+                    urlConnection.getInputStream();
+                    httpResponseCode = urlConnection.getResponseCode();
+                } catch (IOException e) {
+                } finally {
+                    if (urlConnection != null) urlConnection.disconnect();
+                }
+                if (httpResponseCode == 204) {
+                    done(true);
+                }
+            }
+        }).start();
+    }
+
+    private class MyWebViewClient extends WebViewClient {
+        @Override
+        public void onPageStarted(WebView view, String url, Bitmap favicon) {
+            testForCaptivePortal();
+        }
+
+        @Override
+        public void onPageFinished(WebView view, String url) {
+            testForCaptivePortal();
+        }
+    }
+
+    private class MyWebChromeClient extends WebChromeClient {
+        @Override
+        public void onProgressChanged(WebView view, int newProgress) {
+            setProgress(newProgress*100);
+        }
+    }
+}
diff --git a/services/core/java/com/android/server/ConnectivityService.java b/services/core/java/com/android/server/ConnectivityService.java
index 1083c4c..1d0b170 100644
--- a/services/core/java/com/android/server/ConnectivityService.java
+++ b/services/core/java/com/android/server/ConnectivityService.java
@@ -3131,6 +3131,16 @@
                     handleLingerComplete(nai);
                     break;
                 }
+                case NetworkMonitor.EVENT_PROVISIONING_NOTIFICATION: {
+                    NetworkAgentInfo nai = mNetworkAgentInfos.get(msg.replyTo);
+                    if (nai == null) {
+                        loge("EVENT_PROVISIONING_NOTIFICATION from unknown NetworkMonitor");
+                        break;
+                    }
+                    setProvNotificationVisibleIntent(msg.arg1 != 0, nai.networkInfo.getType(),
+                            nai.networkInfo.getExtraInfo(), (PendingIntent)msg.obj);
+                    break;
+                }
                 case NetworkStateTracker.EVENT_STATE_CHANGED: {
                     info = (NetworkInfo) msg.obj;
                     NetworkInfo.State state = info.getState();
@@ -5039,6 +5049,40 @@
             log("setProvNotificationVisible: E visible=" + visible + " networkType=" + networkType
                 + " extraInfo=" + extraInfo + " url=" + url);
         }
+        Intent intent = null;
+        PendingIntent pendingIntent = null;
+        if (visible) {
+            switch (networkType) {
+                case ConnectivityManager.TYPE_WIFI:
+                    intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
+                    intent.setFlags(Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT |
+                            Intent.FLAG_ACTIVITY_NEW_TASK);
+                    pendingIntent = PendingIntent.getActivity(mContext, 0, intent, 0);
+                    break;
+                case ConnectivityManager.TYPE_MOBILE:
+                case ConnectivityManager.TYPE_MOBILE_HIPRI:
+                    intent = new Intent(CONNECTED_TO_PROVISIONING_NETWORK_ACTION);
+                    intent.putExtra("EXTRA_URL", url);
+                    intent.setFlags(0);
+                    pendingIntent = PendingIntent.getBroadcast(mContext, 0, intent, 0);
+                    break;
+                default:
+                    intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
+                    intent.setFlags(Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT |
+                            Intent.FLAG_ACTIVITY_NEW_TASK);
+                    pendingIntent = PendingIntent.getActivity(mContext, 0, intent, 0);
+                    break;
+            }
+        }
+        setProvNotificationVisibleIntent(visible, networkType, extraInfo, pendingIntent);
+    }
+
+    private void setProvNotificationVisibleIntent(boolean visible, int networkType,
+            String extraInfo, PendingIntent intent) {
+        if (DBG) {
+            log("setProvNotificationVisibleIntent: E visible=" + visible + " networkType=" +
+                networkType + " extraInfo=" + extraInfo);
+        }
 
         Resources r = Resources.getSystem();
         NotificationManager notificationManager = (NotificationManager) mContext
@@ -5048,7 +5092,6 @@
             CharSequence title;
             CharSequence details;
             int icon;
-            Intent intent;
             Notification notification = new Notification();
             switch (networkType) {
                 case ConnectivityManager.TYPE_WIFI:
@@ -5056,10 +5099,6 @@
                     details = r.getString(R.string.network_available_sign_in_detailed,
                             extraInfo);
                     icon = R.drawable.stat_notify_wifi_in_range;
-                    intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
-                    intent.setFlags(Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT |
-                            Intent.FLAG_ACTIVITY_NEW_TASK);
-                    notification.contentIntent = PendingIntent.getActivity(mContext, 0, intent, 0);
                     break;
                 case ConnectivityManager.TYPE_MOBILE:
                 case ConnectivityManager.TYPE_MOBILE_HIPRI:
@@ -5068,20 +5107,12 @@
                     // name has been added to it
                     details = mTelephonyManager.getNetworkOperatorName();
                     icon = R.drawable.stat_notify_rssi_in_range;
-                    intent = new Intent(CONNECTED_TO_PROVISIONING_NETWORK_ACTION);
-                    intent.putExtra("EXTRA_URL", url);
-                    intent.setFlags(0);
-                    notification.contentIntent = PendingIntent.getBroadcast(mContext, 0, intent, 0);
                     break;
                 default:
                     title = r.getString(R.string.network_available_sign_in, 0);
                     details = r.getString(R.string.network_available_sign_in_detailed,
                             extraInfo);
                     icon = R.drawable.stat_notify_rssi_in_range;
-                    intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
-                    intent.setFlags(Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT |
-                            Intent.FLAG_ACTIVITY_NEW_TASK);
-                    notification.contentIntent = PendingIntent.getActivity(mContext, 0, intent, 0);
                     break;
             }
 
@@ -5090,6 +5121,7 @@
             notification.flags = Notification.FLAG_AUTO_CANCEL;
             notification.tickerText = title;
             notification.setLatestEventInfo(mContext, title, details, notification.contentIntent);
+            notification.contentIntent = intent;
 
             try {
                 notificationManager.notify(NOTIFICATION_ID, networkType, notification);
diff --git a/services/core/java/com/android/server/connectivity/NetworkMonitor.java b/services/core/java/com/android/server/connectivity/NetworkMonitor.java
index 47789b1..0d3b501 100644
--- a/services/core/java/com/android/server/connectivity/NetworkMonitor.java
+++ b/services/core/java/com/android/server/connectivity/NetworkMonitor.java
@@ -16,17 +16,26 @@
 
 package com.android.server.connectivity;
 
+import android.app.PendingIntent;
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
 import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.net.ConnectivityManager;
+import android.net.Network;
 import android.net.NetworkCapabilities;
 import android.net.NetworkInfo;
 import android.os.Handler;
 import android.os.Message;
 import android.os.SystemProperties;
+import android.os.UserHandle;
 import android.provider.Settings;
 
 import com.android.internal.util.Protocol;
 import com.android.internal.util.State;
 import com.android.internal.util.StateMachine;
+import com.android.server.ConnectivityService;
 import com.android.server.connectivity.NetworkAgentInfo;
 
 import java.io.BufferedReader;
@@ -34,6 +43,7 @@
 import java.io.IOException;
 import java.io.OutputStreamWriter;
 import java.net.HttpURLConnection;
+import java.net.InetAddress;
 import java.net.InetSocketAddress;
 import java.net.Socket;
 import java.net.URL;
@@ -47,6 +57,19 @@
     private static final String DEFAULT_SERVER = "clients3.google.com";
     private static final int SOCKET_TIMEOUT_MS = 10000;
 
+    // Intent broadcast when user selects sign-in notification.
+    private static final String ACTION_SIGN_IN_REQUESTED =
+            "android.net.netmon.sign_in_requested";
+
+    // Keep these in sync with CaptivePortalLoginActivity.java.
+    // Intent broadcast from CaptivePortalLogin indicating sign-in is complete.
+    // Extras:
+    //     EXTRA_TEXT       = netId
+    //     LOGGED_IN_RESULT = "1" if we should use network, "0" if not.
+    private static final String ACTION_CAPTIVE_PORTAL_LOGGED_IN =
+            "android.net.netmon.captive_portal_logged_in";
+    private static final String LOGGED_IN_RESULT = "result";
+
     private static final int BASE = Protocol.BASE_NETWORK_MONITOR;
 
     /**
@@ -87,35 +110,63 @@
     public static final int EVENT_NETWORK_LINGER_COMPLETE = BASE + 5;
 
     /**
-     * Message to self indicating it's time to check for a captive portal again.
-     * TODO - Remove this once broadcast intents are used to communicate with
-     * apps to log into captive portals.
-     * arg1 = Token to ignore old messages.
-     */
-    private static final int CMD_CAPTIVE_PORTAL_REEVALUATE = BASE + 6;
-
-    /**
      * Message to self indicating it's time to evaluate a network's connectivity.
      * arg1 = Token to ignore old messages.
      */
-    private static final int CMD_REEVALUATE = BASE + 7;
+    private static final int CMD_REEVALUATE = BASE + 6;
 
     /**
      * Message to self indicating network evaluation is complete.
      * arg1 = Token to ignore old messages.
      * arg2 = HTTP response code of network evaluation.
      */
-    private static final int EVENT_REEVALUATION_COMPLETE = BASE + 8;
+    private static final int EVENT_REEVALUATION_COMPLETE = BASE + 7;
 
     /**
      * Inform NetworkMonitor that the network has disconnected.
      */
-    public static final int CMD_NETWORK_DISCONNECTED = BASE + 9;
+    public static final int CMD_NETWORK_DISCONNECTED = BASE + 8;
 
     /**
      * Force evaluation even if it has succeeded in the past.
      */
-    public static final int CMD_FORCE_REEVALUATION = BASE + 10;
+    public static final int CMD_FORCE_REEVALUATION = BASE + 9;
+
+    /**
+     * Message to self indicating captive portal login is complete.
+     * arg1 = Token to ignore old messages.
+     * arg2 = 1 if we should use this network, 0 otherwise.
+     */
+    private static final int CMD_CAPTIVE_PORTAL_LOGGED_IN = BASE + 10;
+
+    /**
+     * Message to self indicating user desires to log into captive portal.
+     * arg1 = Token to ignore old messages.
+     */
+    private static final int CMD_USER_WANTS_SIGN_IN = BASE + 11;
+
+    /**
+     * Request ConnectivityService display provisioning notification.
+     * arg1    = Whether to make the notification visible.
+     * obj     = Intent to be launched when notification selected by user.
+     * replyTo = NetworkAgentInfo.messenger so ConnectivityService can identify sender.
+     */
+    public static final int EVENT_PROVISIONING_NOTIFICATION = BASE + 12;
+
+    /**
+     * Message to self indicating sign-in app bypassed captive portal.
+     */
+    private static final int EVENT_APP_BYPASSED_CAPTIVE_PORTAL = BASE + 13;
+
+    /**
+     * Message to self indicating no sign-in app responded.
+     */
+    private static final int EVENT_NO_APP_RESPONSE = BASE + 14;
+
+    /**
+     * Message to self indicating sign-in app indicates sign-in is not possible.
+     */
+    private static final int EVENT_APP_INDICATES_SIGN_IN_IMPOSSIBLE = BASE + 15;
 
     private static final String LINGER_DELAY_PROPERTY = "persist.netmon.linger";
     // Default to 30s linger time-out.
@@ -123,16 +174,17 @@
     private final int mLingerDelayMs;
     private int mLingerToken = 0;
 
-    private static final int CAPTIVE_PORTAL_REEVALUATE_DELAY_MS = 5000;
-    private int mCaptivePortalReevaluateToken = 0;
-
     // Negative values disable reevaluation.
     private static final String REEVALUATE_DELAY_PROPERTY = "persist.netmon.reeval_delay";
     // Default to 5s reevaluation delay.
     private static final int DEFAULT_REEVALUATE_DELAY_MS = 5000;
+    private static final int MAX_RETRIES = 10;
     private final int mReevaluateDelayMs;
     private int mReevaluateToken = 0;
 
+    private int mCaptivePortalLoggedInToken = 0;
+    private int mUserPromptedToken = 0;
+
     private final Context mContext;
     private final Handler mConnectivityServiceHandler;
     private final NetworkAgentInfo mNetworkAgentInfo;
@@ -144,6 +196,9 @@
     private State mOfflineState = new OfflineState();
     private State mValidatedState = new ValidatedState();
     private State mEvaluatingState = new EvaluatingState();
+    private State mUninteractiveAppsPromptedState = new UninteractiveAppsPromptedState();
+    private State mUserPromptedState = new UserPromptedState();
+    private State mInteractiveAppsPromptedState = new InteractiveAppsPromptedState();
     private State mCaptivePortalState = new CaptivePortalState();
     private State mLingeringState = new LingeringState();
 
@@ -159,6 +214,9 @@
         addState(mOfflineState, mDefaultState);
         addState(mValidatedState, mDefaultState);
         addState(mEvaluatingState, mDefaultState);
+        addState(mUninteractiveAppsPromptedState, mDefaultState);
+        addState(mUserPromptedState, mDefaultState);
+        addState(mInteractiveAppsPromptedState, mDefaultState);
         addState(mCaptivePortalState, mDefaultState);
         addState(mLingeringState, mDefaultState);
         setInitialState(mOfflineState);
@@ -171,9 +229,8 @@
         mReevaluateDelayMs = SystemProperties.getInt(REEVALUATE_DELAY_PROPERTY,
                 DEFAULT_REEVALUATE_DELAY_MS);
 
-        // TODO: Enable this when we're ready.
-        // mIsCaptivePortalCheckEnabled = Settings.Global.getInt(mContext.getContentResolver(),
-        //        Settings.Global.CAPTIVE_PORTAL_DETECTION_ENABLED, 1) == 1;
+        mIsCaptivePortalCheckEnabled = Settings.Global.getInt(mContext.getContentResolver(),
+                Settings.Global.CAPTIVE_PORTAL_DETECTION_ENABLED, 1) == 1;
 
         start();
     }
@@ -237,6 +294,8 @@
     }
 
     private class EvaluatingState extends State {
+        private int mRetries;
+
         private class EvaluateInternetConnectivity extends Thread {
             private int mToken;
             EvaluateInternetConnectivity(int token) {
@@ -249,6 +308,7 @@
 
         @Override
         public void enter() {
+            mRetries = 0;
             sendMessage(CMD_REEVALUATE, ++mReevaluateToken, 0);
         }
 
@@ -260,7 +320,7 @@
                     if (message.arg1 != mReevaluateToken)
                         break;
                     // If network provides no internet connectivity adjust evaluation.
-                    if (mNetworkAgentInfo.networkCapabilities.hasCapability(
+                    if (!mNetworkAgentInfo.networkCapabilities.hasCapability(
                             NetworkCapabilities.NET_CAPABILITY_INTERNET)) {
                         // TODO: Try to verify something works.  Do all gateways respond to pings?
                         transitionTo(mValidatedState);
@@ -276,12 +336,12 @@
                     if (httpResponseCode == 204) {
                         transitionTo(mValidatedState);
                     } else if (httpResponseCode >= 200 && httpResponseCode <= 399) {
-                        transitionTo(mCaptivePortalState);
-                    } else {
-                        if (mReevaluateDelayMs >= 0) {
-                            Message msg = obtainMessage(CMD_REEVALUATE, ++mReevaluateToken, 0);
-                            sendMessageDelayed(msg, mReevaluateDelayMs);
-                        }
+                        transitionTo(mUninteractiveAppsPromptedState);
+                    } else if (++mRetries > MAX_RETRIES) {
+                        transitionTo(mOfflineState);
+                    } else if (mReevaluateDelayMs >= 0) {
+                        Message msg = obtainMessage(CMD_REEVALUATE, ++mReevaluateToken, 0);
+                        sendMessageDelayed(msg, mReevaluateDelayMs);
                     }
                     break;
                 default:
@@ -291,30 +351,223 @@
         }
     }
 
-    // TODO: Until we add an intent from the app handling captive portal
-    // login we'll just re-evaluate after a delay.
-    private class CaptivePortalState extends State {
+    private class AppRespondedBroadcastReceiver extends BroadcastReceiver {
+        private static final int CAPTIVE_PORTAL_UNINITIALIZED_RETURN_CODE = 0;
+        private boolean mCanceled;
+        AppRespondedBroadcastReceiver() {
+            mCanceled = false;
+        }
+        public void send(String action) {
+            Intent intent = new Intent(action);
+            intent.putExtra(ConnectivityManager.EXTRA_NETWORK, mNetworkAgentInfo.network);
+            mContext.sendOrderedBroadcastAsUser(intent, UserHandle.ALL, null, this, getHandler(),
+                    CAPTIVE_PORTAL_UNINITIALIZED_RETURN_CODE, null, null);
+        }
+        public void cancel() {
+            mCanceled = true;
+        }
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            if (!mCanceled) {
+                cancel();
+                switch (getResultCode()) {
+                    case ConnectivityManager.CAPTIVE_PORTAL_SIGNED_IN:
+                        sendMessage(EVENT_APP_BYPASSED_CAPTIVE_PORTAL);
+                        break;
+                    case ConnectivityManager.CAPTIVE_PORTAL_DISCONNECT:
+                        sendMessage(EVENT_APP_INDICATES_SIGN_IN_IMPOSSIBLE);
+                        break;
+                    // NOTE: This case label makes compiler enforce no overlap between result codes.
+                    case CAPTIVE_PORTAL_UNINITIALIZED_RETURN_CODE:
+                    default:
+                        sendMessage(EVENT_NO_APP_RESPONSE);
+                        break;
+                }
+            }
+        }
+    }
+
+    private class UninteractiveAppsPromptedState extends State {
+        private AppRespondedBroadcastReceiver mReceiver;
         @Override
         public void enter() {
-            Message message = obtainMessage(CMD_CAPTIVE_PORTAL_REEVALUATE,
-                    ++mCaptivePortalReevaluateToken, 0);
-            sendMessageDelayed(message, CAPTIVE_PORTAL_REEVALUATE_DELAY_MS);
+            mReceiver = new AppRespondedBroadcastReceiver();
+            mReceiver.send(ConnectivityManager.ACTION_CAPTIVE_PORTAL_DETECTED);
+        }
+        @Override
+        public boolean processMessage(Message message) {
+            if (DBG) log(getName() + message.toString());
+            switch (message.what) {
+                case EVENT_APP_BYPASSED_CAPTIVE_PORTAL:
+                    transitionTo(mValidatedState);
+                    break;
+                case EVENT_APP_INDICATES_SIGN_IN_IMPOSSIBLE:
+                    transitionTo(mOfflineState);
+                    break;
+                case EVENT_NO_APP_RESPONSE:
+                    transitionTo(mUserPromptedState);
+                    break;
+                default:
+                    return NOT_HANDLED;
+            }
+            return HANDLED;
+        }
+        public void exit() {
+            mReceiver.cancel();
+        }
+    }
+
+    private class UserPromptedState extends State {
+        private class UserRespondedBroadcastReceiver extends BroadcastReceiver {
+            private final int mToken;
+            UserRespondedBroadcastReceiver(int token) {
+                mToken = token;
+            }
+            @Override
+            public void onReceive(Context context, Intent intent) {
+                if (Integer.parseInt(intent.getStringExtra(Intent.EXTRA_TEXT)) ==
+                        mNetworkAgentInfo.network.netId) {
+                    sendMessage(obtainMessage(CMD_USER_WANTS_SIGN_IN, mToken));
+                }
+            }
+        }
+
+        private UserRespondedBroadcastReceiver mUserRespondedBroadcastReceiver;
+
+        @Override
+        public void enter() {
+            // Wait for user to select sign-in notifcation.
+            mUserRespondedBroadcastReceiver = new UserRespondedBroadcastReceiver(
+                    ++mUserPromptedToken);
+            IntentFilter filter = new IntentFilter(ACTION_SIGN_IN_REQUESTED);
+            mContext.registerReceiver(mUserRespondedBroadcastReceiver, filter);
+            // Initiate notification to sign-in.
+            Intent intent = new Intent(ACTION_SIGN_IN_REQUESTED);
+            intent.putExtra(Intent.EXTRA_TEXT, String.valueOf(mNetworkAgentInfo.network.netId));
+            Message message = obtainMessage(EVENT_PROVISIONING_NOTIFICATION, 1, 0,
+                    PendingIntent.getBroadcast(mContext, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT));
+            message.replyTo = mNetworkAgentInfo.messenger;
+            mConnectivityServiceHandler.sendMessage(message);
         }
 
         @Override
         public boolean processMessage(Message message) {
             if (DBG) log(getName() + message.toString());
             switch (message.what) {
-                case CMD_CAPTIVE_PORTAL_REEVALUATE:
-                    if (message.arg1 != mCaptivePortalReevaluateToken)
+                case CMD_USER_WANTS_SIGN_IN:
+                    if (message.arg1 != mUserPromptedToken)
                         break;
-                    transitionTo(mEvaluatingState);
+                    transitionTo(mInteractiveAppsPromptedState);
                     break;
                 default:
                     return NOT_HANDLED;
             }
             return HANDLED;
         }
+
+        @Override
+        public void exit() {
+            Message message = obtainMessage(EVENT_PROVISIONING_NOTIFICATION, 0, 0, null);
+            message.replyTo = mNetworkAgentInfo.messenger;
+            mConnectivityServiceHandler.sendMessage(message);
+            mContext.unregisterReceiver(mUserRespondedBroadcastReceiver);
+            mUserRespondedBroadcastReceiver = null;
+        }
+    }
+
+    private class InteractiveAppsPromptedState extends State {
+        private AppRespondedBroadcastReceiver mReceiver;
+        @Override
+        public void enter() {
+            mReceiver = new AppRespondedBroadcastReceiver();
+            mReceiver.send(ConnectivityManager.ACTION_CAPTIVE_PORTAL_SIGN_IN);
+        }
+        @Override
+        public boolean processMessage(Message message) {
+            if (DBG) log(getName() + message.toString());
+            switch (message.what) {
+                case EVENT_APP_BYPASSED_CAPTIVE_PORTAL:
+                    transitionTo(mValidatedState);
+                    break;
+                case EVENT_APP_INDICATES_SIGN_IN_IMPOSSIBLE:
+                    transitionTo(mOfflineState);
+                    break;
+                case EVENT_NO_APP_RESPONSE:
+                    transitionTo(mCaptivePortalState);
+                    break;
+                default:
+                    return NOT_HANDLED;
+            }
+            return HANDLED;
+        }
+        public void exit() {
+            mReceiver.cancel();
+        }
+    }
+
+    private class CaptivePortalState extends State {
+        private class CaptivePortalLoggedInBroadcastReceiver extends BroadcastReceiver {
+            private final int mToken;
+
+            CaptivePortalLoggedInBroadcastReceiver(int token) {
+                mToken = token;
+            }
+
+            @Override
+            public void onReceive(Context context, Intent intent) {
+                if (Integer.parseInt(intent.getStringExtra(Intent.EXTRA_TEXT)) ==
+                        mNetworkAgentInfo.network.netId) {
+                    sendMessage(obtainMessage(CMD_CAPTIVE_PORTAL_LOGGED_IN, mToken,
+                            Integer.parseInt(intent.getStringExtra(LOGGED_IN_RESULT))));
+                }
+            }
+        }
+
+        private CaptivePortalLoggedInBroadcastReceiver mCaptivePortalLoggedInBroadcastReceiver;
+
+        @Override
+        public void enter() {
+            Intent intent = new Intent(Intent.ACTION_SEND);
+            intent.putExtra(Intent.EXTRA_TEXT, String.valueOf(mNetworkAgentInfo.network.netId));
+            intent.setType("text/plain");
+            intent.setComponent(new ComponentName("com.android.captiveportallogin",
+                    "com.android.captiveportallogin.CaptivePortalLoginActivity"));
+            intent.setFlags(Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT | Intent.FLAG_ACTIVITY_NEW_TASK);
+
+            // Wait for result.
+            mCaptivePortalLoggedInBroadcastReceiver = new CaptivePortalLoggedInBroadcastReceiver(
+                    ++mCaptivePortalLoggedInToken);
+            IntentFilter filter = new IntentFilter(ACTION_CAPTIVE_PORTAL_LOGGED_IN);
+            mContext.registerReceiver(mCaptivePortalLoggedInBroadcastReceiver, filter);
+            // Initiate app to log in.
+            mContext.startActivityAsUser(intent, UserHandle.CURRENT);
+        }
+
+        @Override
+        public boolean processMessage(Message message) {
+            if (DBG) log(getName() + message.toString());
+            switch (message.what) {
+                case CMD_CAPTIVE_PORTAL_LOGGED_IN:
+                    if (message.arg1 != mCaptivePortalLoggedInToken)
+                        break;
+                    if (message.arg2 == 0) {
+                        // TODO: Should teardown network.
+                        transitionTo(mOfflineState);
+                    } else {
+                        transitionTo(mValidatedState);
+                    }
+                    break;
+                default:
+                    return NOT_HANDLED;
+            }
+            return HANDLED;
+        }
+
+        @Override
+        public void exit() {
+            mContext.unregisterReceiver(mCaptivePortalLoggedInBroadcastReceiver);
+            mCaptivePortalLoggedInBroadcastReceiver = null;
+        }
     }
 
     private class LingeringState extends State {
@@ -353,10 +606,12 @@
         if (!mIsCaptivePortalCheckEnabled) return 204;
 
         String urlString = "http://" + mServer + "/generate_204";
-        if (DBG) log("Checking " + urlString);
+        if (DBG) {
+            log("Checking " + urlString + " on " + mNetworkAgentInfo.networkInfo.getExtraInfo());
+        }
         HttpURLConnection urlConnection = null;
         Socket socket = null;
-        int httpResponseCode = 500;
+        int httpResponseCode = 599;
         try {
             URL url = new URL(urlString);
             if (false) {
@@ -369,25 +624,65 @@
                 urlConnection.getInputStream();
                 httpResponseCode = urlConnection.getResponseCode();
             } else {
-                socket = new Socket();
-                // TODO: setNetworkForSocket(socket, mNetworkAgentInfo.network.netId);
-                InetSocketAddress address = new InetSocketAddress(url.getHost(), 80);
-                // TODO: address = new InetSocketAddress(
-                //               getByNameOnNetwork(mNetworkAgentInfo.network, url.getHost()), 80);
-                socket.connect(address);
+                socket = mNetworkAgentInfo.network.getSocketFactory().createSocket();
+                socket.setSoTimeout(SOCKET_TIMEOUT_MS);
+                // Lookup addresses only on this Network.
+                InetAddress[] hostAddresses = mNetworkAgentInfo.network.getAllByName(url.getHost());
+                // Try all addresses.
+                for (int i = 0; i < hostAddresses.length; i++) {
+                    if (DBG) log("Connecting to " + hostAddresses[i]);
+                    try {
+                        socket.connect(new InetSocketAddress(hostAddresses[i],
+                                url.getDefaultPort()), SOCKET_TIMEOUT_MS);
+                        break;
+                    } catch (IOException e) {
+                        // Ignore exceptions on all but the last.
+                        if (i == (hostAddresses.length - 1)) throw e;
+                    }
+                }
+                if (DBG) log("Requesting " + url.getFile());
                 BufferedReader reader = new BufferedReader(
                         new InputStreamReader(socket.getInputStream()));
                 OutputStreamWriter writer = new OutputStreamWriter(socket.getOutputStream());
-                writer.write("GET " + url.getFile() + " HTTP/1.1\r\n\n");
+                writer.write("GET " + url.getFile() + " HTTP/1.1\r\nHost: " + url.getHost() +
+                        "\r\nConnection: close\r\n\r\n");
                 writer.flush();
                 String response = reader.readLine();
-                if (response.startsWith("HTTP/1.1 ")) {
+                if (DBG) log("Received \"" + response + "\"");
+                if (response != null && (response.startsWith("HTTP/1.1 ") ||
+                        // NOTE: We may want to consider an "HTTP/1.0 204" response to be a captive
+                        // portal.  The only example of this seen so far was a captive portal.  For
+                        // the time being go with prior behavior of assuming it's not a captive
+                        // portal.  If it is considered a captive portal, a different sign-in URL
+                        // is needed (i.e. can't browse a 204).  This could be the result of an HTTP
+                        // proxy server.
+                        response.startsWith("HTTP/1.0 "))) {
+                    // NOTE: We may want to consider an "200" response with "Content-length=0" to
+                    // not be a captive portal. This could be the result of an HTTP proxy server.
+                    // See b/9972012.
                     httpResponseCode = Integer.parseInt(response.substring(9, 12));
+                } else {
+                    // A response was received but not understood.  The fact that a
+                    // response was sent indicates there's some kind of responsive network
+                    // out there so put up the notification to the user to log into the network
+                    // so the user can have the final say as to whether the network is useful.
+                    httpResponseCode = 399;
+                    while (DBG && response != null && !response.isEmpty()) {
+                        try {
+                            response = reader.readLine();
+                        } catch (IOException e) {
+                            break;
+                        }
+                        log("Received \"" + response + "\"");
+                    }
                 }
             }
             if (DBG) log("isCaptivePortal: ret=" + httpResponseCode);
         } catch (IOException e) {
             if (DBG) log("Probably not a portal: exception " + e);
+            if (httpResponseCode == 599) {
+                // TODO: Ping gateway and DNS server and log results.
+            }
         } finally {
             if (urlConnection != null) {
                 urlConnection.disconnect();