Track latency of captive portal checks

When captive portal check occurs, track its latency, whether or not
we received a response, and whether or not the response was a captive
portal.  Pair with information identifying the access point / base
station, and broadcast it (with a system|signature-protected
permission).

Broadcast only occurs if user has consented to
Settings.Global.WIFI_SCAN_ALWAYS_AVAILABLE.

Change-Id: I6fd59954a7ee2cc7acedf064a1465882653b2173
diff --git a/core/java/android/net/CaptivePortalTracker.java b/core/java/android/net/CaptivePortalTracker.java
index 21995c0..19d74ed 100644
--- a/core/java/android/net/CaptivePortalTracker.java
+++ b/core/java/android/net/CaptivePortalTracker.java
@@ -28,11 +28,23 @@
 import android.database.ContentObserver;
 import android.net.ConnectivityManager;
 import android.net.IConnectivityManager;
+import android.net.wifi.WifiInfo;
+import android.net.wifi.WifiManager;
 import android.os.Handler;
 import android.os.UserHandle;
 import android.os.Message;
 import android.os.RemoteException;
+import android.os.SystemClock;
 import android.provider.Settings;
+import android.telephony.CellIdentityCdma;
+import android.telephony.CellIdentityGsm;
+import android.telephony.CellIdentityLte;
+import android.telephony.CellIdentityWcdma;
+import android.telephony.CellInfo;
+import android.telephony.CellInfoCdma;
+import android.telephony.CellInfoGsm;
+import android.telephony.CellInfoLte;
+import android.telephony.CellInfoWcdma;
 import android.telephony.TelephonyManager;
 
 import com.android.internal.util.State;
@@ -44,6 +56,7 @@
 import java.net.Inet4Address;
 import java.net.URL;
 import java.net.UnknownHostException;
+import java.util.List;
 
 import com.android.internal.R;
 
@@ -60,12 +73,29 @@
 
     private static final int SOCKET_TIMEOUT_MS = 10000;
 
+    public static final String ACTION_NETWORK_CONDITIONS_MEASURED =
+            "android.net.conn.NETWORK_CONDITIONS_MEASURED";
+    public static final String EXTRA_CONNECTIVITY_TYPE = "extra_connectivity_type";
+    public static final String EXTRA_NETWORK_TYPE = "extra_network_type";
+    public static final String EXTRA_RESPONSE_RECEIVED = "extra_response_received";
+    public static final String EXTRA_IS_CAPTIVE_PORTAL = "extra_is_captive_portal";
+    public static final String EXTRA_CELL_ID = "extra_cellid";
+    public static final String EXTRA_SSID = "extra_ssid";
+    public static final String EXTRA_BSSID = "extra_bssid";
+    /** real time since boot */
+    public static final String EXTRA_REQUEST_TIMESTAMP_MS = "extra_request_timestamp_ms";
+    public static final String EXTRA_RESPONSE_TIMESTAMP_MS = "extra_response_timestamp_ms";
+
+    private static final String PERMISSION_ACCESS_NETWORK_CONDITIONS =
+            "android.permission.ACCESS_NETWORK_CONDITIONS";
+
     private String mServer;
     private String mUrl;
     private boolean mNotificationShown = false;
     private boolean mIsCaptivePortalCheckEnabled = false;
     private IConnectivityManager mConnService;
     private TelephonyManager mTelephonyManager;
+    private WifiManager mWifiManager;
     private Context mContext;
     private NetworkInfo mNetworkInfo;
 
@@ -92,6 +122,7 @@
         mContext = context;
         mConnService = cs;
         mTelephonyManager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
+        mWifiManager = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
         mProvisioningObserver = new ProvisioningObserver();
 
         IntentFilter filter = new IntentFilter();
@@ -319,7 +350,8 @@
     }
 
     /**
-     * Do a URL fetch on a known server to see if we get the data we expect
+     * Do a URL fetch on a known server to see if we get the data we expect.
+     * Measure the response time and broadcast that.
      */
     private boolean isCaptivePortal(InetAddress server) {
         HttpURLConnection urlConnection = null;
@@ -327,6 +359,7 @@
 
         mUrl = "http://" + server.getHostAddress() + "/generate_204";
         if (DBG) log("Checking " + mUrl);
+        long requestTimestamp = -1;
         try {
             URL url = new URL(mUrl);
             urlConnection = (HttpURLConnection) url.openConnection();
@@ -334,11 +367,26 @@
             urlConnection.setConnectTimeout(SOCKET_TIMEOUT_MS);
             urlConnection.setReadTimeout(SOCKET_TIMEOUT_MS);
             urlConnection.setUseCaches(false);
+
+            // Time how long it takes to get a response to our request
+            requestTimestamp = SystemClock.elapsedRealtime();
+
             urlConnection.getInputStream();
+
+            // Time how long it takes to get a response to our request
+            long responseTimestamp = SystemClock.elapsedRealtime();
+
             // we got a valid response, but not from the real google
-            return urlConnection.getResponseCode() != 204;
+            boolean isCaptivePortal = urlConnection.getResponseCode() != 204;
+
+            sendNetworkConditionsBroadcast(true /* response received */, isCaptivePortal,
+                    requestTimestamp, responseTimestamp);
+            return isCaptivePortal;
         } catch (IOException e) {
             if (DBG) log("Probably not a portal: exception " + e);
+            if (requestTimestamp != -1) {
+                sendFailedCaptivePortalCheckBroadcast(requestTimestamp);
+            } // else something went wrong with setting up the urlConnection
             return false;
         } finally {
             if (urlConnection != null) {
@@ -352,12 +400,15 @@
         try {
             inetAddress = InetAddress.getAllByName(hostname);
         } catch (UnknownHostException e) {
+            sendFailedCaptivePortalCheckBroadcast(SystemClock.elapsedRealtime());
             return null;
         }
 
         for (InetAddress a : inetAddress) {
             if (a instanceof Inet4Address) return a;
         }
+
+        sendFailedCaptivePortalCheckBroadcast(SystemClock.elapsedRealtime());
         return null;
     }
 
@@ -414,4 +465,80 @@
         }
         mNotificationShown = visible;
     }
+
+    private void sendFailedCaptivePortalCheckBroadcast(long requestTimestampMs) {
+        sendNetworkConditionsBroadcast(false /* response received */, false /* ignored */,
+                requestTimestampMs, 0 /* ignored */);
+    }
+
+    /**
+     * @param responseReceived - whether or not we received a valid HTTP response to our request.
+     * If false, isCaptivePortal and responseTimestampMs are ignored
+     */
+    private void sendNetworkConditionsBroadcast(boolean responseReceived, boolean isCaptivePortal,
+            long requestTimestampMs, long responseTimestampMs) {
+        if (Settings.Global.getInt(mContext.getContentResolver(),
+                Settings.Global.WIFI_SCAN_ALWAYS_AVAILABLE, 0) == 0) {
+            if (DBG) log("Don't send network conditions - lacking user consent.");
+            return;
+        }
+
+        Intent latencyBroadcast = new Intent(ACTION_NETWORK_CONDITIONS_MEASURED);
+        switch (mNetworkInfo.getType()) {
+            case ConnectivityManager.TYPE_WIFI:
+                WifiInfo currentWifiInfo = mWifiManager.getConnectionInfo();
+                if (currentWifiInfo != null) {
+                    latencyBroadcast.putExtra(EXTRA_SSID, currentWifiInfo.getSSID());
+                    latencyBroadcast.putExtra(EXTRA_BSSID, currentWifiInfo.getBSSID());
+                } else {
+                    if (DBG) logw("network info is TYPE_WIFI but no ConnectionInfo found");
+                    return;
+                }
+                break;
+            case ConnectivityManager.TYPE_MOBILE:
+                latencyBroadcast.putExtra(EXTRA_NETWORK_TYPE, mTelephonyManager.getNetworkType());
+                List<CellInfo> info = mTelephonyManager.getAllCellInfo();
+                if (info == null) return;
+                StringBuffer uniqueCellId = new StringBuffer();
+                int numRegisteredCellInfo = 0;
+                for (CellInfo cellInfo : info) {
+                    if (cellInfo.isRegistered()) {
+                        numRegisteredCellInfo++;
+                        if (numRegisteredCellInfo > 1) {
+                            if (DBG) log("more than one registered CellInfo.  Can't " +
+                                    "tell which is active.  Bailing.");
+                            return;
+                        }
+                        if (cellInfo instanceof CellInfoCdma) {
+                            CellIdentityCdma cellId = ((CellInfoCdma) cellInfo).getCellIdentity();
+                            latencyBroadcast.putExtra(EXTRA_CELL_ID, cellId);
+                        } else if (cellInfo instanceof CellInfoGsm) {
+                            CellIdentityGsm cellId = ((CellInfoGsm) cellInfo).getCellIdentity();
+                            latencyBroadcast.putExtra(EXTRA_CELL_ID, cellId);
+                        } else if (cellInfo instanceof CellInfoLte) {
+                            CellIdentityLte cellId = ((CellInfoLte) cellInfo).getCellIdentity();
+                            latencyBroadcast.putExtra(EXTRA_CELL_ID, cellId);
+                        } else if (cellInfo instanceof CellInfoWcdma) {
+                            CellIdentityWcdma cellId = ((CellInfoWcdma) cellInfo).getCellIdentity();
+                            latencyBroadcast.putExtra(EXTRA_CELL_ID, cellId);
+                        } else {
+                            if (DBG) logw("Registered cellinfo is unrecognized");
+                            return;
+                        }
+                    }
+                }
+                break;
+            default:
+                return;
+        }
+        latencyBroadcast.putExtra(EXTRA_CONNECTIVITY_TYPE, mNetworkInfo.getType());
+        latencyBroadcast.putExtra(EXTRA_RESPONSE_RECEIVED, responseReceived);
+        latencyBroadcast.putExtra(EXTRA_REQUEST_TIMESTAMP_MS, requestTimestampMs);
+
+        if (responseReceived) {
+            latencyBroadcast.putExtra(EXTRA_IS_CAPTIVE_PORTAL, isCaptivePortal);
+            latencyBroadcast.putExtra(EXTRA_RESPONSE_TIMESTAMP_MS, responseTimestampMs);
+        }
+        mContext.sendBroadcast(latencyBroadcast, PERMISSION_ACCESS_NETWORK_CONDITIONS);
+    }
 }
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index a9a14ad..d106cf2 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -201,6 +201,7 @@
     <protected-broadcast android:name="android.net.wifi.p2p.PERSISTENT_GROUPS_CHANGED" />
     <protected-broadcast android:name="android.net.conn.TETHER_STATE_CHANGED" />
     <protected-broadcast android:name="android.net.conn.INET_CONDITION_ACTION" />
+    <protected-broadcast android:name="android.net.conn.NETWORK_CONDITIONS_MEASURED" />
     <protected-broadcast android:name="android.intent.action.EXTERNAL_APPLICATIONS_AVAILABLE" />
     <protected-broadcast android:name="android.intent.action.EXTERNAL_APPLICATIONS_UNAVAILABLE" />
     <protected-broadcast android:name="android.intent.action.AIRPLANE_MODE" />
@@ -2399,6 +2400,13 @@
         android:description="@string/permdesc_invokeCarrierSetup"
         android:protectionLevel="signature|system" />
 
+    <!-- Allows an application to listen for network condition observations.
+         @hide This is not a third-party API (intended for system apps). -->
+    <permission android:name="android.permission.ACCESS_NETWORK_CONDITIONS"
+        android:label="@string/permlab_accessNetworkConditions"
+        android:description="@string/permdesc_accessNetworkConditions"
+        android:protectionLevel="signature|system" />
+
     <!-- The system process is explicitly the only one allowed to launch the
          confirmation UI for full backup/restore -->
     <uses-permission android:name="android.permission.CONFIRM_FULL_BACKUP"/>
diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml
index d3dbb2a..1b5ee68 100644
--- a/core/res/res/values/strings.xml
+++ b/core/res/res/values/strings.xml
@@ -1905,6 +1905,11 @@
     <!-- Description of an application permission, listed so the user can choose whether they want to allow the application to do this. -->
     <string name="permdesc_invokeCarrierSetup">Allows the holder to invoke the carrier-provided configuration app. Should never be needed for normal apps.</string>
 
+    <!-- Title of an application permission, listed so the user can choose whether they want to allow the application to do this. -->
+    <string name="permlab_accessNetworkConditions">listen for observations on network conditions</string>
+    <!-- Description of an application permission, listed so the user can choose whether they want to allow the application to do this. -->
+    <string name="permdesc_accessNetworkConditions">Allows an application to listen for observations on network conditions. Should never be needed for normal apps.</string>
+
     <!-- Policy administration -->
 
     <!-- Title of policy access to limiting the user's password choices -->