New Settings symbols for captive portal detection

This patch defines new Settings symbols for
  - setting the probe urls for captive portal detection.
  - setting which User-Agent to use for captive portal detection.

The existing default values for these settings are not changed, i.e:
  - HTTP and HTTPS probes urls are unchanged.
  - the fallback probe is not used.
  - User-Agent is empty by default.

Bug: 29367974
Change-Id: I6e4b3b172e56b8b67fffa4b51f776d68d5851f25
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index c9b03cc..3cb46a7 100755
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -7960,12 +7960,37 @@
         /**
          * The server used for captive portal detection upon a new conection. A
          * 204 response code from the server is used for validation.
+         * TODO: remove this deprecated symbol.
          *
          * @hide
          */
         public static final String CAPTIVE_PORTAL_SERVER = "captive_portal_server";
 
         /**
+         * The URL used for HTTPS captive portal detection upon a new connection.
+         * A 204 response code from the server is used for validation.
+         *
+         * @hide
+         */
+        public static final String CAPTIVE_PORTAL_HTTPS_URL = "captive_portal_https_url";
+
+        /**
+         * The URL used for HTTP captive portal detection upon a new connection.
+         * A 204 response code from the server is used for validation.
+         *
+         * @hide
+         */
+        public static final String CAPTIVE_PORTAL_HTTP_URL = "captive_portal_http_url";
+
+        /**
+         * The URL used for fallback HTTP captive portal detection when previous HTTP
+         * and HTTPS captive portal detection attemps did not return a conclusive answer.
+         *
+         * @hide
+         */
+        public static final String CAPTIVE_PORTAL_FALLBACK_URL = "captive_portal_fallback_url";
+
+        /**
          * Whether to use HTTPS for network validation. This is enabled by default and the setting
          * needs to be set to 0 to disable it. This setting is a misnomer because captive portals
          * don't actually use HTTPS, but it's consistent with the other settings.
@@ -7975,6 +8000,14 @@
         public static final String CAPTIVE_PORTAL_USE_HTTPS = "captive_portal_use_https";
 
         /**
+         * Which User-Agent string to use in the header of the captive portal detection probes.
+         * The User-Agent field is unset when this setting has no value (HttpUrlConnection default).
+         *
+         * @hide
+         */
+        public static final String CAPTIVE_PORTAL_USER_AGENT = "captive_portal_user_agent";
+
+        /**
          * Whether network service discovery is enabled.
          *
          * @hide
diff --git a/services/core/java/com/android/server/ConnectivityService.java b/services/core/java/com/android/server/ConnectivityService.java
index 524c3cc..b7dfd19 100644
--- a/services/core/java/com/android/server/ConnectivityService.java
+++ b/services/core/java/com/android/server/ConnectivityService.java
@@ -5501,7 +5501,7 @@
 
     @Override
     public String getCaptivePortalServerUrl() {
-        return NetworkMonitor.getCaptivePortalServerUrl(mContext);
+        return NetworkMonitor.getCaptivePortalServerHttpUrl(mContext);
     }
 
     @Override
diff --git a/services/core/java/com/android/server/connectivity/NetworkMonitor.java b/services/core/java/com/android/server/connectivity/NetworkMonitor.java
index 01a2cad..13f3174 100644
--- a/services/core/java/com/android/server/connectivity/NetworkMonitor.java
+++ b/services/core/java/com/android/server/connectivity/NetworkMonitor.java
@@ -79,11 +79,20 @@
  * {@hide}
  */
 public class NetworkMonitor extends StateMachine {
-    private static final boolean DBG = false;
     private static final String TAG = NetworkMonitor.class.getSimpleName();
-    private static final String DEFAULT_SERVER = "connectivitycheck.gstatic.com";
+    private static final boolean DBG = false;
+
+    // Default urls for captive portal detection probes
+    private static final String DEFAULT_HTTPS_URL     =
+            "https://connectivitycheck.gstatic.com/generate_204";
+    private static final String DEFAULT_HTTP_URL      =
+            "http://connectivitycheck.gstatic.com/generate_204";
+    private static final String DEFAULT_FALLBACK_URL  = null;
+    private static final String DEFAULT_USER_AGENT    = null;
+
     private static final int SOCKET_TIMEOUT_MS = 10000;
-    private static final int PROBE_TIMEOUT_MS = 3000;
+    private static final int PROBE_TIMEOUT_MS  = 3000;
+
     public static final String ACTION_NETWORK_CONDITIONS_MEASURED =
             "android.net.conn.NETWORK_CONDITIONS_MEASURED";
     public static final String EXTRA_CONNECTIVITY_TYPE = "extra_connectivity_type";
@@ -596,22 +605,33 @@
         }
     }
 
-    private static String getCaptivePortalServerUrl(Context context, boolean isHttps) {
-        String server = Settings.Global.getString(context.getContentResolver(),
-                Settings.Global.CAPTIVE_PORTAL_SERVER);
-        if (server == null) server = DEFAULT_SERVER;
-        return (isHttps ? "https" : "http") + "://" + server + "/generate_204";
+    private static String getCaptivePortalServerHttpsUrl(Context context) {
+        return getSetting(context, Settings.Global.CAPTIVE_PORTAL_HTTPS_URL, DEFAULT_HTTPS_URL);
     }
 
-    public static String getCaptivePortalServerUrl(Context context) {
-        return getCaptivePortalServerUrl(context, false);
+    public static String getCaptivePortalServerHttpUrl(Context context) {
+        return getSetting(context, Settings.Global.CAPTIVE_PORTAL_HTTP_URL, DEFAULT_HTTP_URL);
+    }
+
+    private static String getCaptivePortalFallbackUrl(Context context) {
+        return getSetting(context,
+                Settings.Global.CAPTIVE_PORTAL_FALLBACK_URL, DEFAULT_FALLBACK_URL);
+    }
+
+    private static String getCaptivePortalUserAgent(Context context) {
+        return getSetting(context, Settings.Global.CAPTIVE_PORTAL_USER_AGENT, DEFAULT_USER_AGENT);
+    }
+
+    private static String getSetting(Context context, String symbol, String defaultValue) {
+        final String value = Settings.Global.getString(context.getContentResolver(), symbol);
+        return value != null ? value : defaultValue;
     }
 
     @VisibleForTesting
     protected CaptivePortalProbeResult isCaptivePortal() {
         if (!mIsCaptivePortalCheckEnabled) return new CaptivePortalProbeResult(204);
 
-        URL pacUrl = null, httpUrl = null, httpsUrl = null;
+        URL pacUrl = null, httpsUrl = null, httpUrl = null, fallbackUrl = null;
 
         // On networks with a PAC instead of fetching a URL that should result in a 204
         // response, we instead simply fetch the PAC script.  This is done for a few reasons:
@@ -632,20 +652,17 @@
         //    results for network validation.
         final ProxyInfo proxyInfo = mNetworkAgentInfo.linkProperties.getHttpProxy();
         if (proxyInfo != null && !Uri.EMPTY.equals(proxyInfo.getPacFileUrl())) {
-            try {
-                pacUrl = new URL(proxyInfo.getPacFileUrl().toString());
-            } catch (MalformedURLException e) {
-                validationLog("Invalid PAC URL: " + proxyInfo.getPacFileUrl().toString());
+            pacUrl = makeURL(proxyInfo.getPacFileUrl().toString());
+            if (pacUrl == null) {
                 return CaptivePortalProbeResult.FAILED;
             }
         }
 
         if (pacUrl == null) {
-            try {
-                httpUrl = new URL(getCaptivePortalServerUrl(mContext, false));
-                httpsUrl = new URL(getCaptivePortalServerUrl(mContext, true));
-            } catch (MalformedURLException e) {
-                validationLog("Bad validation URL: " + getCaptivePortalServerUrl(mContext, false));
+            httpsUrl = makeURL(getCaptivePortalServerHttpsUrl(mContext));
+            httpUrl = makeURL(getCaptivePortalServerHttpUrl(mContext));
+            fallbackUrl = makeURL(getCaptivePortalFallbackUrl(mContext));
+            if (httpUrl == null || httpsUrl == null) {
                 return CaptivePortalProbeResult.FAILED;
             }
         }
@@ -691,7 +708,7 @@
         if (pacUrl != null) {
             result = sendHttpProbe(pacUrl, ValidationProbeEvent.PROBE_PAC);
         } else if (mUseHttps) {
-            result = sendParallelHttpProbes(httpsUrl, httpUrl, null);
+            result = sendParallelHttpProbes(httpsUrl, httpUrl, fallbackUrl);
         } else {
             result = sendHttpProbe(httpUrl, ValidationProbeEvent.PROBE_HTTP);
         }
@@ -721,6 +738,10 @@
             urlConnection.setConnectTimeout(SOCKET_TIMEOUT_MS);
             urlConnection.setReadTimeout(SOCKET_TIMEOUT_MS);
             urlConnection.setUseCaches(false);
+            final String userAgent = getCaptivePortalUserAgent(mContext);
+            if (userAgent != null) {
+               urlConnection.setRequestProperty("User-Agent", userAgent);
+            }
 
             // Time how long it takes to get a response to our request
             long requestTimestamp = SystemClock.elapsedRealtime();
@@ -846,6 +867,17 @@
         return httpsProbe.result();
     }
 
+    private URL makeURL(String url) {
+        if (url != null) {
+            try {
+                return new URL(url);
+            } catch (MalformedURLException e) {
+                validationLog("Bad URL: " + url);
+            }
+        }
+        return null;
+    }
+
     /**
      * @param responseReceived - whether or not we received a valid HTTP response to our request.
      * If false, isCaptivePortal and responseTimestampMs are ignored